Swift Testing Framework บน Xcode 26: คู่มือฉบับสมบูรณ์แทน XCTest (2026)

คู่มือ Swift Testing framework ใน Xcode 26 พร้อมตัวอย่างการใช้งาน @Test, #expect, parameterized tests, @Suite, traits และวิธีย้ายจาก XCTest ทีละขั้นตอน

Swift Testing บน Xcode 26: คู่มือแทน XCTest

อัปเดตล่าสุด: 2 กรกฎาคม 2026

Swift Testing คือเฟรมเวิร์กการทดสอบยุคใหม่ของ Apple ที่ออกแบบมาแทนที่ XCTest โดยใช้ Swift macros อย่าง @Test และ #expect เพื่อให้การเขียน test สั้นลง อ่านง่ายขึ้น และรองรับ Swift Concurrency แบบเนทีฟ ในปี 2026 Swift Testing ได้กลายเป็นมาตรฐานหลักที่มาพร้อม Xcode 26 และ Swift 6.2 พร้อมฟีเจอร์ครบถ้วนทั้ง parameterized tests, traits, และ test suites สำหรับโปรเจกต์ iOS, macOS, และ Swift Server-side ทุกประเภท

  • Swift Testing ใช้ @Test macro แทนการตั้งชื่อฟังก์ชันขึ้นต้นด้วย test เหมือน XCTest ทำให้ตั้งชื่อได้อิสระและมี display name แบบภาษาไทยได้
  • #expect ใช้สำหรับ assertion ทั่วไป ส่วน #require จะหยุดการทำงานของ test ทันทีเมื่อเงื่อนไขไม่ผ่าน แทน XCTAssert ทั้งชุด
  • Parameterized tests ด้วย arguments: ทำให้ทดสอบหลายเคสในฟังก์ชันเดียวและเห็นผลลัพธ์แยกรายเคสใน Xcode 26
  • Traits อย่าง .tags, .disabled, .bug, และ .timeLimit ช่วยจัดการเมทาดาต้าของ test อย่างเป็นระบบ
  • Swift Testing รองรับ async/await เนทีฟ ไม่ต้องใช้ expectation กับ fulfill เหมือน XCTest อีกต่อไป
  • ยังคงต้องใช้ XCTest สำหรับ UI tests, performance tests และเทสต์ที่พึ่งพา XCUIApplication อยู่

Swift Testing คืออะไร และทำไมต้องเปลี่ยนจาก XCTest?

Swift Testing คือเฟรมเวิร์กการทดสอบโอเพนซอร์สที่ทีม Swift พัฒนาขึ้นตั้งแต่ปี 2024 และผนวกเข้าเป็นส่วนหนึ่งของ Swift Toolchain อย่างเป็นทางการตั้งแต่ Xcode 16 เป็นต้นมา ในปี 2026 กับ Xcode 26 มันไม่ใช่ตัวเลือกเสริมอีกต่อไป แต่กลายเป็นค่าเริ่มต้นเมื่อสร้างโปรเจกต์ใหม่ ผมได้ย้ายทีมที่ผมดูแลจาก XCTest มาใช้ Swift Testing ตั้งแต่ปีที่แล้ว และพบว่าโค้ด test สั้นลงเฉลี่ย 30-40% เพราะไม่ต้องเขียน boilerplate ยาว ๆ อีกต่อไป

เหตุผลหลักที่ Apple ผลักดัน Swift Testing มีสามข้อ หนึ่ง คือการใช้ macros ทำให้ error message ชัดเจนกว่าเดิมมาก เพราะ compiler สามารถขยาย #expect(a == b) ให้เห็นค่าซ้ายและขวาโดยอัตโนมัติ ไม่ต้องเขียน message เพิ่มเอง สอง คือการรองรับ Swift Concurrency แบบเนทีฟ ไม่ต้องเขียน expectation กับ fulfill() ที่ดูเทอะทะเหมือน XCTest และสาม คือระบบ traits ที่ทำให้จัดกลุ่ม test ด้วย tags, บันทึกลิงก์ bug และตั้ง timeout ต่อ test ได้โดยไม่ต้องดัดแปลง test runner

นอกจากนี้ Swift Testing ยังทำงานคู่ขนานกับ XCTest ในโปรเจกต์เดียวกันได้ทันที คุณไม่จำเป็นต้องย้ายทุกอย่างในคราวเดียว ค่อย ๆ เขียน test ใหม่ด้วย Swift Testing แล้วปล่อย test เก่าไว้ก่อนก็ได้ ทำให้การเปลี่ยนผ่านเป็นเรื่องไม่เจ็บปวด

เริ่มต้นใช้งาน Swift Testing ใน Xcode 26

Swift Testing มาพร้อม Xcode 26 โดยไม่ต้องติดตั้งอะไรเพิ่ม แค่สร้างโปรเจกต์ใหม่ก็จะพบว่า test target ถูกตั้งค่าเป็น Swift Testing เป็นค่าเริ่มต้นแล้ว หากคุณมีโปรเจกต์เดิมที่ใช้ XCTest อยู่ ให้เปิด test target แล้วเพิ่มไฟล์ Swift ใหม่ปกติ ไม่ต้องสร้าง target แยก

สำหรับโปรเจกต์ Swift Package Manager ให้เพิ่ม dependency ใน Package.swift ดังนี้:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "MyLibrary",
    platforms: [.iOS(.v17), .macOS(.v14)],
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    targets: [
        .target(name: "MyLibrary"),
        .testTarget(
            name: "MyLibraryTests",
            dependencies: ["MyLibrary"]
        ),
    ]
)

ตั้งแต่ Swift 6.0 เป็นต้นมา Swift Testing รวมอยู่ใน Swift Toolchain โดยตรง คุณไม่ต้องประกาศ dependency แยกอีก แค่ import Testing ในไฟล์ test ก็ใช้งานได้ทันที เมื่อจะรัน test ให้กด Cmd+U ใน Xcode หรือใช้คำสั่ง swift test ใน Terminal ผลลัพธ์จะแสดงในรูปแบบใหม่ที่จัดกลุ่มตาม Suite และแสดง display name ที่คุณตั้ง

@Test macro และการเขียน test แรกของคุณ

หัวใจของ Swift Testing คือ @Test macro ที่มาแทนที่ธรรมเนียมการตั้งชื่อฟังก์ชันขึ้นต้นด้วย test ใน XCTest ข้อดีที่เห็นได้ชัดคือคุณตั้งชื่อฟังก์ชันเป็นภาษาอังกฤษปกติได้ และยังใส่ display name แบบภาษาไทยให้แสดงใน Xcode ได้อีกด้วย

import Testing
@testable import MyLibrary

@Test("บวกเลขสองจำนวนได้ผลลัพธ์ถูกต้อง")
func addition() {
    let calculator = Calculator()
    #expect(calculator.add(2, 3) == 5)
}

@Test("หารด้วยศูนย์ควรโยน DivisionError")
func divisionByZero() throws {
    let calculator = Calculator()
    #expect(throws: DivisionError.zeroDivisor) {
        try calculator.divide(10, by: 0)
    }
}

สังเกตว่าเราไม่ต้องสร้าง class ที่ inherit จาก XCTestCase อีกต่อไป ฟังก์ชัน test เป็น global function หรือ method ใน struct ก็ได้ ทำให้จัดระเบียบไฟล์ได้ยืดหยุ่นกว่าเดิมมาก และเนื่องจาก Swift Testing สร้าง instance ใหม่ต่อ test ทุกครั้ง ไม่มี state รั่วระหว่าง test เหมือน XCTest ที่แชร์ instance ในบางกรณี ตามที่อธิบายไว้ในเอกสารทางการของ Apple เรื่อง Swift Testing

สำหรับผู้ที่ทำงานกับ concurrency อยู่ เราแนะนำให้อ่านคู่มือ Swift 6.2 Approachable Concurrencyควบคู่กันไป เพราะ Swift Testing รองรับ async test เต็มรูปแบบและใช้ pattern เดียวกันกับโค้ด production

#expect vs #require: การตรวจสอบผลลัพธ์แบบใหม่

Swift Testing มี macro ตรวจสอบเงื่อนไขหลักอยู่สองตัวคือ #expect และ #require ซึ่งทำหน้าที่ต่างกัน #expect จะบันทึกความล้มเหลวแต่ยังทำงานต่อไป เหมือน XCTAssert ทั่วไป ส่วน #require จะโยน error และหยุด test ทันที เหมาะกับกรณีที่ค่าถัดไปพึ่งพาค่าปัจจุบัน

@Test func fetchUserProfile() async throws {
    let response = try await api.fetchUser(id: 42)
    let user = try #require(response.user)
    #expect(user.id == 42)
    #expect(user.email.contains("@"))
    #expect(user.name.isEmpty == false)
}

ในตัวอย่างข้างบน ถ้า response.user เป็น nil #require จะโยน error ทันที ทำให้ test หยุด ไม่ต้องไปเจอ crash จากการ force-unwrap ในบรรทัดถัดไป ต่างจาก XCTest ที่ต้อง guard let user = response.user else { XCTFail(); return } ยาว ๆ

จุดเด่นอีกอย่างของ macro เหล่านี้คือ error message ที่ละเอียดมาก เวลา #expect(user.id == 42) ไม่ผ่าน Xcode จะแสดงทั้งค่าจริงและค่าที่คาดหวังโดยที่คุณไม่ต้องเขียน message เพิ่มเอง (feature นี้เรียกว่า "expression diagnostics") ซึ่งเป็นสิ่งที่ทำให้ macro-based testing เหนือกว่าฟังก์ชันธรรมดา

Parameterized Tests: ทดสอบหลายเคสด้วยฟังก์ชันเดียว

ฟีเจอร์ที่ผู้ใช้ XCTest รอคอยมานานคือ parameterized tests ที่รันฟังก์ชันเดียวหลายรอบด้วยข้อมูลต่างกัน โดย Xcode 26 จะแสดงผลลัพธ์แยกทีละเคสในหน้า test navigator ทำให้รู้ทันทีว่าเคสไหนล้มเหลว

@Test("ตรวจสอบรูปแบบอีเมล", arguments: [
    ("[email protected]", true),
    ("invalid-email", false),
    ("test@localhost", false),
    ("[email protected]", true),
])
func emailValidation(input: String, isValid: Bool) {
    let validator = EmailValidator()
    #expect(validator.isValid(input) == isValid)
}

คุณสามารถส่งได้ทั้ง array ธรรมดา, tuple, หรือแม้กระทั่งสอง collection ที่จะถูก zip เข้าด้วยกันโดยอัตโนมัติ นี่คือการปรับปรุงครั้งใหญ่จาก XCTest ที่ต้องเขียน for loop ใน test แต่ละอันเอง ทำให้ report บอกไม่ได้ว่าเคสที่เท่าไรที่ล้มเหลว

@Test(arguments: [1, 2, 3, 4, 5], [1, 4, 9, 16, 25])
func squareRoot(number: Int, expectedSquare: Int) {
    #expect(number * number == expectedSquare)
}

เมื่อมีจำนวนอาร์กิวเมนต์มาก ผมมักแยกออกไปเป็น static property เพื่อให้อ่านง่าย และสามารถแชร์ระหว่าง test หลายตัวได้ ในทีมของผมพบว่าการใช้ parameterized tests ลดจำนวนไฟล์ test ลงได้ครึ่งหนึ่งในกรณีที่มี validation logic ซับซ้อนหลายเงื่อนไข

@Suite และการจัดกลุ่ม test

@Suite macro ใช้จัดกลุ่ม test ที่เกี่ยวข้องกันไว้ใน struct หรือ class เดียวกัน คล้ายกับ XCTestCase แต่ยืดหยุ่นกว่ามาก และรองรับการซ้อน suite ภายในกันได้

@Suite("ระบบยืนยันตัวตน", .tags(.authentication))
struct AuthenticationTests {
    let authService: AuthService

    init() async throws {
        // แทน setUp() ของ XCTest (สร้าง instance ใหม่ทุก test)
        self.authService = try await AuthService.forTesting()
    }

    @Test func signInSuccess() async throws {
        let result = try await authService.signIn(email: "[email protected]", password: "1234")
        #expect(result.isAuthenticated)
    }

    @Suite("การรีเซ็ตรหัสผ่าน")
    struct PasswordResetTests {
        @Test func sendResetEmail() async throws {
            // test ซ้อน suite
        }
    }
}

จุดเปลี่ยนสำคัญคือ Swift Testing สร้าง instance ใหม่ของ Suite struct ก่อน test แต่ละตัว ทำให้ init ทำหน้าที่แทน setUp() โดยธรรมชาติ และไม่จำเป็นต้องมี tearDown() เพราะ Swift จะเรียก deinit ให้เองหลัง test จบ นี่เป็น pattern ที่ปลอดภัยต่อ state และเหมาะกับ Swift Concurrency มากกว่า

Traits: Tags, .disabled, .bug และ .timeLimit

Traits คือระบบเมทาดาต้าที่ทำให้ควบคุมพฤติกรรมของ test ได้ละเอียด โดยไม่ต้องแทรก if statement ในโค้ด test เลย traits ที่ใช้บ่อยที่สุดมีดังนี้

extension Tag {
    @Tag static var integration: Self
    @Tag static var slow: Self
}

@Test(
    "ดึงข้อมูลจาก API จริง",
    .tags(.integration, .slow),
    .timeLimit(.minutes(1)),
    .bug("https://github.com/company/repo/issues/1234", "รอ backend fix")
)
func fetchLiveData() async throws {
    let data = try await api.fetchLiveMetrics()
    #expect(data.count > 0)
}

@Test("ฟีเจอร์ที่ยังไม่พร้อม", .disabled("รอ backend release เดือนหน้า"))
func upcomingFeature() {
    // test นี้จะข้ามไปโดยอัตโนมัติ
}

Trait ที่มีประโยชน์มากคือ .bug() ที่ผูก test เข้ากับ issue tracker โดยตรง เมื่อ test ล้มเหลว Xcode จะแสดงลิงก์ให้คลิกไปดู issue นั้นได้ทันที ส่วน .tags() ช่วยกรอง test เวลารันบน CI เช่นรัน .tags(.integration) เฉพาะกลางคืน และรัน .tags(.unit) ทุก commit ตามที่แนะนำในคู่มืออย่างเป็นทางการของ Swift Testing

.timeLimit() ใช้จำกัดเวลาต่อ test เพื่อป้องกัน test hang บน CI ซึ่งเป็นปัญหาที่พบบ่อยเมื่อทำงานกับโค้ด async ที่ deadlock

การทดสอบ async/await และ Confirmation

Swift Testing รองรับ async/await เนทีฟ 100% แค่ทำเครื่องหมายฟังก์ชัน test เป็น async ก็เขียน await ได้เลย ไม่ต้องพึ่ง XCTestExpectation กับ fulfill() เหมือน XCTest ที่มักเขียนพลาดจนเกิด race condition

@Test func downloadImage() async throws {
    let downloader = ImageDownloader()
    let image = try await downloader.download(from: URL(string: "https://example.com/image.png")!)
    #expect(image.size.width > 0)
}

สำหรับ event-driven code ที่ต้องรอ callback หรือ notification หลายครั้ง Swift Testing มี confirmation() API ที่ทำงานได้ปลอดภัยกว่า XCTestExpectation เพราะรวมกับ Structured Concurrency เต็มตัว

@Test func webSocketReceivesThreeMessages() async throws {
    let socket = TestWebSocket()

    await confirmation("รับข้อความ 3 ครั้ง", expectedCount: 3) { confirm in
        socket.onMessage = { _ in
            confirm()
        }
        try? await socket.simulate(messages: ["a", "b", "c"])
        try? await Task.sleep(for: .milliseconds(100))
    }
}

ถ้า closure จบก่อนที่ confirm() จะถูกเรียกครบ 3 ครั้ง Swift Testing จะรายงานว่า test ล้มเหลวโดยระบุจำนวนที่ขาดหายไป ทำให้ debug ง่ายกว่า XCTest หลายเท่า สิ่งนี้เข้ากันได้ดีกับ Swift Concurrency อย่างที่ผมได้เขียนไว้ในบทความFoundation Models Framework ใน iOS 26ที่มีการทำงาน async เยอะมาก

วิธีย้ายจาก XCTest ไปยัง Swift Testing

สำหรับทีมที่มี XCTest suite ขนาดใหญ่อยู่แล้ว การย้ายไปใช้ Swift Testing สามารถทำได้ทีละส่วนโดยไม่กระทบ test เดิม ผมแนะนำ 5 ขั้นตอนดังนี้

  1. เปิด import Testing เพิ่มในไฟล์ที่ต้องการทดลอง: Swift Testing และ XCTest ทำงานร่วมกันในไฟล์เดียวได้ (แต่ต่างฟังก์ชัน)
  2. เขียน test ใหม่ด้วย Swift Testing เท่านั้น: ห้ามใจร้อนแปลง test เดิมยกชุด รอให้คนในทีมคุ้นเคยก่อน
  3. เมื่อพร้อม ให้ใช้ Xcode 26 Convert Assistant: ตัวช่วยแปลง XCTAssertEqual, XCTAssertTrue ให้เป็น #expect แบบอัตโนมัติได้ในหลายกรณี
  4. แทน setUp/tearDown ด้วย init/deinit: ระวังลำดับการทำงานเปลี่ยน โดย Swift Testing จะสร้าง instance ใหม่ทุก test เสมอ
  5. รัน test ทั้งชุดบน CI ก่อน merge: ตรวจสอบให้แน่ใจว่า test ที่แปลงมาไม่มี hidden state จากการแชร์ instance เดิม

ตารางเทียบ macro หลักที่คุณจะใช้ระหว่างแปลง:

XCTest APISwift Testing เทียบเท่าหมายเหตุ
XCTAssertEqual(a, b)#expect(a == b)Message อัตโนมัติ ไม่ต้องเขียนเอง
XCTAssertTrue(x)#expect(x)เหมือน XCTest แต่กระชับกว่า
XCTUnwrap(optional)try #require(optional)หยุด test เมื่อ nil
XCTAssertThrowsError#expect(throws: Error.self)ระบุประเภท error ที่คาดหวังได้
setUp() / tearDown()init() / deinitinstance ใหม่ทุก test
XCTestExpectationawait confirmation()Async native ไม่มี race
XCTSkip.disabled(if:) traitประกาศที่ level ของ test

ข้อจำกัดและกรณีที่ควรใช้ XCTest ต่อ

แม้ Swift Testing จะครอบคลุมแทบทุกกรณีในปี 2026 แต่ยังมีบางส่วนที่ต้องใช้ XCTest ต่อไป โดยเฉพาะการเขียน UI tests ด้วย XCUIApplication ที่ยังไม่มีคู่ขนานใน Swift Testing เต็มรูปแบบ Apple ประกาศว่าฟีเจอร์นี้จะทยอยย้ายมา แต่จนถึงกลางปี 2026 คุณยังต้องสร้าง XCTestCase สำหรับ UI test อยู่

อีกกรณีคือ performance tests ด้วย measure { } block ที่ยังคงอยู่ใน XCTest เท่านั้น รวมทั้งการทดสอบที่ต้องพึ่ง XCUIElement หรือ accessibility APIs สำหรับหน้าจอ ถ้าโปรเจกต์คุณมี test เหล่านี้อยู่ ให้ปล่อยไว้ที่ XCTest ต่อไปและใช้ Swift Testing สำหรับ unit test เพียงอย่างเดียวก็เพียงพอ

สุดท้าย บางไลบรารีเก่าที่พึ่ง waitForExpectations หรือ Objective-C runtime อาจไม่ทำงานกับ Swift Testing โดยตรง กรณีนี้ทางออกที่ผมใช้บ่อยคือเขียน adapter ให้ closure-based API ที่ Swift Testing เรียกใช้ได้ ซึ่งใช้เวลาไม่กี่นาทีต่อไลบรารี

คำถามที่พบบ่อย

Swift Testing กับ XCTest ต่างกันอย่างไร?

Swift Testing ใช้ macros อย่าง @Test และ #expect ทำให้ syntax สั้นและ error message ละเอียดกว่า รองรับ async/await เนทีฟ, parameterized tests, และ traits ในขณะที่ XCTest ยังคงเป็นตัวเลือกสำหรับ UI tests และ performance tests

ต้องเลิกใช้ XCTest เลยไหมเมื่อเปลี่ยนมา Swift Testing?

ไม่ต้อง Swift Testing ทำงานคู่ขนานกับ XCTest ในโปรเจกต์เดียวกันได้ เขียน test ใหม่ด้วย Swift Testing โดยไม่กระทบ test เก่าเลย และยังต้องใช้ XCTest สำหรับ UI tests, performance tests และ Objective-C-based tests

Swift Testing รองรับ mocking หรือไม่?

Swift Testing ไม่ได้ให้ mocking framework ในตัว แต่ทำงานได้กับทุก mocking library ที่คุณใช้อยู่ เช่น Cuckoo, Mockingbird หรือ hand-rolled protocol-based mocks ที่พบบ่อยในโค้ด Swift สมัยใหม่

รัน test เฉพาะบางกลุ่มด้วย tags ได้อย่างไร?

ใน Xcode 26 กด Cmd+Shift+U แล้วเลือก tag ที่ต้องการรัน หรือใน command line ใช้ swift test --filter tag:integration เพื่อรันเฉพาะ test ที่มี trait .tags(.integration)

Swift Testing ใช้บน Linux ได้หรือไม่?

ได้ Swift Testing เป็นโอเพนซอร์สและรองรับ Linux, Windows, macOS, และ Apple platforms ทั้งหมด ทำให้เหมาะกับโปรเจกต์ Swift Server-side ด้วย เพียงระบุ swift-tools-version 6.0 ขึ้นไปใน Package.swift

Editorial Team
เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.