← 所有文章

Swift Testing:取代 XCTest 的框架,以及 XCTest 中保留的部分

XCTest 是 Apple 自 2013 年以來的測試框架,正被 Swift Testing 取代。Apple 尚未明說「已棄用」(XCTest 仍隨附、仍可運作、仍承載 UI 自動化和效能測試),但自 WWDC 2024 以來,每場 Apple 講座、每個 Swift 論壇討論串、每個新範例專案,都將 Swift 程式碼導向 Swift Testing。此框架隨 Xcode 16+ 和 Swift 6 一起推出1,可在 macOS、iOS、watchOS、tvOS、visionOS 和 Linux 上執行2,並呈現截然不同的思維模型:測試是函式、套件是型別、設定是特徵、並行是預設。

遷移是漸進式的。Apple 自家文件明確支援在同一目標中並排執行兩種框架3,這意味著對於非平凡的程式碼庫而言,正確的做法不是「全部重寫」,而是「以 Swift Testing 撰寫新測試,並在接觸舊測試時遷移它們」。本文走過 API 介面,點出遷移的邊界(哪些保留在 XCTest),並涵蓋取代 XCTest 類別階層設定的特徵詞彙。

TL;DR

  • Swift Testing 以巨集取代 XCTest 基於類別、字串型別的模型:@Test@Suite#expect(...)#require(...)4
  • 測試是自由函式或型別上的方法;套件是任何包含測試函式的型別(只有在指定顯示名稱或特徵時才需要 @Suite)。
  • 設定從類別階層和命名慣例移轉至特徵:.enabled(if:).disabled(_:).serialized.timeLimit(...).tags(...).bug(...)5
  • 透過 @Test(arguments:) 進行參數化測試會產生一個父測試結果,每個參數一個子結果;預設並行6
  • 遷移是並排式而非一次性。UI 自動化(XCUIApplication)和效能測試(XCTMetric)保留在 XCTest,因為 Swift Testing 不涵蓋這些3
  • WWDC 2025 新增了自訂附件(測試產物)和退出測試(驗證在特定條件下預期會終止的程式碼)7

為何需要 Swift Testing

凡是維護過大型 iOS 測試套件的人,都熟悉 XCTest 的痛點:

字串型別的斷言。XCTAssertEqual(a, b, "message") 不會擷取原始的 Swift 表達式。當斷言失敗時,失敗訊息告訴你執行時的值,但不告訴你哪個表達式產生了它們。除錯需要閱讀測試原始碼並重建脈絡。

基於類別的設定。設定、清理和共用狀態需要繼承 XCTestCase。停用測試需要重新命名(讓探索模式遺漏它)或將其包裹在 #if DEBUG 中。挑選要執行的測試需要謹慎命名。

並行性不匹配。XCTest 早於 Swift Concurrency。XCTestExpectation 加上 wait(for:timeout:) 是橋接到非同步程式碼的舊有方式;該模式冗長且容易出錯。現代 Swift 程式碼使用 async/await;XCTest 的慣用法感覺過時。

參數化能力薄弱。XCTest 沒有一級的參數化測試支援。開發者用測試方法內的 for 迴圈偽造(將多個案例產生為一個測試結果),或撰寫程式碼產生器(從範本產生 N 個測試方法)。

Swift Testing 解決了每一項:

import Testing

@Test func userInitializesWithDefaults() {
    let user = User()
    #expect(user.name == "Anonymous")
    #expect(user.preferences.isEmpty)
}

#expect 擷取完整的表達式。當斷言失敗時,測試輸出顯示 user.name == "Anonymous" 失敗,因為 user.name"Guest"。框架完成讀取表達式的工作,因為巨集看得到語法樹。沒有「預期 X,得到 Y」的猜測。

巨集

四個巨集承擔了所有工作。

@Test@Suite

@Test 將函式標記為測試。函式可以位於檔案範圍、結構中、actor 中、列舉中,只要 Swift 中函式可以存在的地方都行。框架會探索並執行 @Test 標記的函式。

@Test func loginSucceedsWithValidCredentials() async throws {
    let result = try await api.login(username: "alice", password: "valid")
    #expect(result.isAuthenticated)
}

struct AuthenticationTests {
    @Test func loginRejectsEmptyPassword() async throws {
        let result = try await api.login(username: "alice", password: "")
        #expect(!result.isAuthenticated)
        #expect(result.error == .emptyPassword)
    }
}

@Suite 將型別明確標記為測試套件。當型別包含 @Test 函式時,此標記是隱含的;只有在加入自訂顯示名稱或在套件層級套用特徵時才需要明確的 @Suite4

@Suite("Authentication", .tags(.security))
struct AuthenticationTests {
    @Test func loginSucceeds() async throws { ... }
    @Test func loginFails() async throws { ... }
}

#expect#require

#expect 是標準斷言。如果預期失敗,測試會繼續(失敗會被記錄;後續斷言仍會執行)。#require 是快速失敗的變體:如果要求失敗,測試會立即停止。對於失敗會在後續斷言中產生無意義錯誤的前置條件,請使用 #require

@Test func userPreferencesLoad() async throws {
    let user = try #require(await store.fetchUser(id: 42))
    // 如果 fetchUser 回傳 nil,測試在此停止。下一行
    // 永不會對未解包的 optional 執行。
    #expect(user.preferences["theme"] == "dark")
    #expect(user.preferences.count >= 1)
}

#require 前面的 try 很重要:當要求滿足時,#require 回傳已解包的值(在此案例中是非 optional 的 User),否則會擲出例外。測試函式必須是 throws 才能使用 #require 進行解包。

參數化測試

@Test(arguments:) 會為每個參數執行一次測試函式。框架將每個參數回報為子測試,父測試作為彙總。

@Test(arguments: [
    ("alice", "password123", true),
    ("bob", "", false),
    ("", "password123", false),
    ("carol", "wrongpassword", false),
])
func loginScenario(username: String, password: String, expectedSuccess: Bool) async throws {
    let result = try await api.login(username: username, password: password)
    #expect(result.isAuthenticated == expectedSuccess)
}

框架預設會並行執行這四個參數。測試失敗訊息會指出哪個參數元組失敗,因此除錯參數化失敗無需單獨執行測試。

對於相互獨立的集合參數,@Test(arguments: arrayA, arrayB) 會執行交叉乘積(每種組合)。對於成對的順序組合,請使用 zip(arrayA, arrayB) 並傳入產生的序列。

特徵:作為一級值的設定

XCTest 使用類別階層、命名慣例和 Xcode 測試計畫來設定測試。Swift Testing 使用特徵:套用至 @Test@Suite 宣告的值5

@Test(.enabled(if: ProcessInfo.processInfo.isRunningOnCI))
func ciOnlyIntegrationTest() async throws { ... }

@Test(.disabled("Pending fix for FB12345"))
func knownFailingTest() { ... }

@Test(.timeLimit(.minutes(1)))
func potentiallySlowTest() async throws { ... }

@Test(.tags(.network, .integration), .bug("FB67890", "regression in retry logic"))
func networkRetryTest() async throws { ... }

@Suite(.serialized)
struct DatabaseTests {
    // 此套件中的所有測試會依序執行。
    // 當測試共用資料庫 fixture 時很有用。
    @Test func insertUser() { ... }
    @Test func updateUser() { ... }
    @Test func deleteUser() { ... }
}

特徵詞彙涵蓋了 XCTest 過去需要自訂基礎設施才能處理的案例:

  • .enabled(if:).disabled(_:)使用執行時表達式和選擇性原因進行條件啟用。取代 #if DEBUG 區塊和重新命名的小技巧。
  • .serialized將套件或參數化測試標記為依序執行。可繼承:序列化套件會強制所有子測試依序執行6
  • .timeLimit(...)每個測試的時間限制。如果測試超過限制,會被終止並回報為失敗。
  • .tags(...)用於篩選的標籤。在 Xcode 測試計畫中設定包含/排除標籤規則;該計畫透過 xcodebuild test -testPlan <name> 執行。請注意 -only-testing: 是依測試識別碼(目標、套件或函式)篩選,而非依標籤。
  • .bug(...)將測試與 bug ID 連結並附帶原因。連結會出現在測試報告中,並(對於已知失敗的測試)防止失敗阻塞 CI。

也支援自訂特徵。應用程式可以為專案特定的需求定義自己的特徵(例如 .requiresKeychain.skipIfOffline)。

並行是預設

Swift Testing 預設並行執行測試,包括參數化參數。預設值很重要,因為大多數測試套件會隨時間增長,並行性是 30 秒測試執行與 5 分鐘測試執行的差別。XCTest 需要明確選擇加入(測試計畫中的測試並行切換);Swift Testing 翻轉了預設值。

含義是:測試必須相互獨立。共用狀態(資料庫 fixture、模擬檔案系統、應用程式層級的單例)必須要嘛每個測試獨立隔離(每個測試取得新的 fixture),要嘛標記為 .serialized(套件依序執行)。會修改全域狀態並假設依序執行的測試是 bug,Swift Testing 會比 XCTest 更快讓它們浮現。

遷移:並排,而非一次性

Apple 對遷移的官方立場是漸進式的3

  • 兩個框架在同一個測試目標中共存。
  • 新測試可以立即用 Swift Testing 撰寫。
  • 舊的 XCTest 測試無需變更即可繼續運作。
  • 在方便時(接觸到的檔案、新功能、重構)將 XCTest 測試遷移到 Swift Testing。

有兩種情況會無限期保留在 XCTest:

UI 自動化測試。XCUIApplicationXCUIElementXCUIElementQuery,整個 UI 測試 API,僅限 XCTest3。Swift Testing 沒有提供替代方案。UI 測試會留在 XCTest,直到 Apple 擴展 Swift Testing 的涵蓋範圍。

效能測試。XCTMetricmeasure(metrics:)、效能基準,也僅限 XCTest3。Swift Testing 尚未有對等項目。

對於其他所有項目(單元測試、整合測試、非同步測試、參數化測試),Swift Testing 是新程式碼的建議預設值。

WWDC 2025 新增了什麼

WWDC 2025 為 Swift Testing 加入了兩項值得注意的新增功能7

自訂附件。測試可將任意 Attachable 資料附加到其結果中,以便進行失敗分類。失敗截圖、診斷紀錄、產生的輸入檔案。附件會出現在 Xcode 的測試報告器和 CI 產物中。Sources/Testing/Attachments/Attachment.swift 中的 Attachment.record(_:named:sourceLocation:) API 定義了介面;接受符合 Attachable 的值(Data、String、透過 opt-in 一致性的影像型別)。

@Test func reportLayoutAtFailure() async throws {
    let result = await renderer.run(LayoutTestCase.complex)
    if !result.passes {
        Attachment.record(result.diagnostics, named: "diagnostics.txt")
        Attachment.record(result.screenshotData, named: "failure-screenshot.png")
    }
    #expect(result.passes)
}

退出測試。驗證預期會終止的程式碼(呼叫 exit()、擲出致命錯誤、觸發前置條件失敗)的測試。XCTest 沒有清晰的方式測試這些;Swift Testing 的退出測試會在子行程中執行程式碼,並驗證終止行為。

@Test func parserExitsOnMalformedInput() async {
    await #expect(processExitsWith: .failure) {
        Parser.parse(malformedInput)
    }
}

兩項新增都很務實。自訂附件解決了真正的痛點(除錯不穩定的 CI 失敗);退出測試涵蓋了單元測試介面中真實存在的缺口。

何時 XCTest 仍是正確選擇

新程式碼仍應使用 XCTest 的三種情況:

測試是 UI 自動化測試。XCUIApplication.launch、點擊與滑動互動、基於 accessibility-identifier 的查詢。Swift Testing 不涵蓋 UI 自動化介面;新的 UI 測試是 XCTest。

測試是效能基準。measure(metrics: [XCTClockMetric()])、效能基準、記憶體量測。Swift Testing 沒有對等項目;新的效能測試是 XCTest。

測試必須在不支援 Swift Testing 的平台上執行。Swift Testing 需要 Swift 6 / Xcode 16+。必須針對較早工具鏈編譯的測試(包含舊版 Xcode 的 CI 矩陣)需要 XCTest,直到工具鏈跟上為止。

對於 Swift 6+ 目標上的單元測試、整合測試、非同步測試和參數化測試,Swift Testing 是現代的預設值。遷移成本是真實的,但不遷移的代價會累積;今日所撰寫的每個新 XCTest 都是技術債。

與代理工作流程的連結

測試是 AI 輔助程式碼生成所遵循的合約。當代理撰寫 Swift 程式碼時,測試是開發者(或代理本身)驗證變更正確性的方式。Swift Testing 的特性讓代理產生的程式碼更易於驗證:

  • 表達式擷取。#expect(user.score == calculator.compute(user)) 失敗會顯示比較的兩側。閱讀失敗的代理可以修復 bug,無需重新閱讀測試檔案。
  • 參數化案例。修復回歸的代理會知道哪個特定輸入失敗。修復可以有針對性。
  • 基於特徵的篩選。.tags(.regression) 註解讓代理在修復後僅執行回歸測試;回饋迴路更快。
  • 自訂附件。除錯不穩定測試的代理可以附加失敗輸入產物,並在下次執行時讀取它。

Apple 開發的 hooks 一文涵蓋了在每次 Claude Code 編輯後執行測試的 Stop hooks。Swift Testing 更快、更並行、更具資訊性的失敗讓該迴路足夠緊密,使得代理可以無需手動介入即可迭代。

此模式對 iOS 26+ 應用程式的意義

三個重點。

  1. 新測試預設使用 Swift Testing。框架更具表達力、預設並行執行、自動擷取失敗脈絡,並將參數化測試作為一級概念支援。現有的 XCTest 程式碼繼續運作;新程式碼使用現代預設值。

  2. 將 UI 自動化和效能測試保留在 XCTest。這是 Apple 的涵蓋缺口,並非遷移選擇。在 Swift Testing 擴展到這些領域之前,這些測試會無限期留在 XCTest。

  3. 將特徵作為設定詞彙使用。.enabled(if:).disabled(_:).serialized.timeLimit(...).tags(...).bug(...) 涵蓋了 XCTest 過去需要類別階層、命名慣例和 #if DEBUG 區塊才能處理的案例。特徵是可組合的一級值;特徵詞彙是讓框架感覺現代的關鍵。

完整的 Apple 生態系叢集:型別化的 App IntentsMCP 伺服器路由問題Foundation Models執行時與工具 LLM 的區別三個介面單一事實來源模式兩個 MCP 伺服器Apple 開發的 hooksLive ActivitieswatchOS 執行時SwiftUI 內部RealityKit 的空間思維模型SwiftData 結構紀律Liquid Glass 模式多平台出貨平台矩陣Vision 框架Symbol EffectsCore ML 推論Writing Tools API我拒絕撰寫的內容。樞紐在 Apple 生態系列。如需更廣泛的「iOS 搭配 AI 代理」脈絡,請參閱 iOS 代理開發指南

FAQ

Swift Testing 會完全取代 XCTest 嗎?

尚未。XCTest 仍承載 UI 自動化測試(XCUIApplication)和效能測試(XCTMetric);Swift Testing 不涵蓋這些領域。對於單元測試、整合測試、非同步測試和參數化測試,Swift Testing 是新程式碼的建議預設值。兩個框架可以在同一個目標中共存而不衝突。

#expectXCTAssertEqual 之間的實際差異是什麼?

#expect 擷取完整的 Swift 表達式,並在失敗時回報比較的兩側(例如 user.score == calculator.compute(user) 失敗,因為 user.score = 80calculator.compute(user) = 100)。XCTAssertEqual 只知道執行時的值和選擇性的訊息字串。表達式擷取讓 Swift Testing 的失敗無需重新閱讀測試原始碼即可自我說明。

參數化測試如何運作?

@Test(arguments: [...]) 會為每個參數執行一次測試函式。框架將每個參數回報為子測試結果,父測試作為彙總。元組可以傳遞多個值。參數化測試預設並行執行;加入 .serialized 可強制依序執行。

#require 用來做什麼?

#require#expect 的快速失敗變體。對於失敗會在後續斷言中產生無意義錯誤的前置條件,請使用它。try #require(value) 在滿足時回傳已解包的值,否則擲出例外,因此測試的其餘部分可以直接使用已解包的值。測試函式必須是 throws 才能使用 #require 進行解包。

我可以在 Linux 上撰寫 Swift Testing 測試嗎?

可以。Swift Testing 是開源的2,可在 Swift 支援的每個平台上執行,包括 Linux。伺服器端 Swift 專案可以立即採用 Swift Testing。此框架並非 Apple 平台專屬。

如何遷移現有的 XCTest 套件?

Apple 的建議是漸進式的:以 Swift Testing 撰寫新測試,在接觸舊測試時遷移它們3。沒有自動轉換器(模型差異夠大,使得自動化會很脆弱)。正確的遷移目標是單元測試和整合測試;UI 測試和效能測試會無限期留在 XCTest。

參考資料


  1. Apple Developer:Swift Testing。Apple 對 Swift Testing 隨 Xcode 16+ 和 Swift 6 採用的概覽。 

  2. 開源儲存庫:GitHub 上的 swiftlang/swift-testing。框架的原始碼,依 Apache License 2.0 與 Swift Project 標準授權提供。 

  3. Apple Developer Documentation:從 XCTest 遷移測試。Apple 的官方遷移指南,涵蓋並排共存以及 XCUIApplication / XCTMetric 例外。 

  4. Apple Developer Documentation:Swift Testing@Test@Suite#expect#require 和特徵詞彙的框架參考。 

  5. Apple Developer Documentation:Trait。定義測試特徵的協定,內建特徵包括 .enabled(if:).disabled(_:).serialized.timeLimit(...).tags(...).bug(...)。 

  6. Apple Developer:Meet Swift Testing(WWDC 2024 議程 10179)和 Go further with Swift Testing(WWDC 2024 議程 10195)。涵蓋參數化測試和並行性的介紹議程。 

  7. WWDC 2025 新增功能的開源儲存庫證據:Sources/Testing/Attachments/Attachment.swift 用於自訂附件,Sources/Testing/ExitTests/ 用於 #expect(processExitsWith:) 退出測試巨集。兩者均首次出現在 WWDC 2025,並隨 Xcode 26+ 一起推出。 

相關文章

Five Apple Platforms, Three Shared Files: How Return Actually Ships Cross-Platform SwiftUI

Return runs on iPhone, iPad, Mac, Apple Watch, and Apple TV. Three Swift files are shared across all five targets out of…

18 分鐘閱讀

What SwiftUI Is Made Of

SwiftUI is a result-builder DSL on top of a value-typed View tree. Once the substrate is visible, AnyView, Group, and Vi…

17 分鐘閱讀

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 分鐘閱讀