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 自動化測試。XCUIApplication、XCUIElement、XCUIElementQuery,整個 UI 測試 API,僅限 XCTest3。Swift Testing 沒有提供替代方案。UI 測試會留在 XCTest,直到 Apple 擴展 Swift Testing 的涵蓋範圍。
效能測試。XCTMetric、measure(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+ 應用程式的意義
三個重點。
-
新測試預設使用 Swift Testing。框架更具表達力、預設並行執行、自動擷取失敗脈絡,並將參數化測試作為一級概念支援。現有的 XCTest 程式碼繼續運作;新程式碼使用現代預設值。
-
將 UI 自動化和效能測試保留在 XCTest。這是 Apple 的涵蓋缺口,並非遷移選擇。在 Swift Testing 擴展到這些領域之前,這些測試會無限期留在 XCTest。
-
將特徵作為設定詞彙使用。
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)涵蓋了 XCTest 過去需要類別階層、命名慣例和#if DEBUG區塊才能處理的案例。特徵是可組合的一級值;特徵詞彙是讓框架感覺現代的關鍵。
完整的 Apple 生態系叢集:型別化的 App Intents;MCP 伺服器;路由問題;Foundation Models;執行時與工具 LLM 的區別;三個介面;單一事實來源模式;兩個 MCP 伺服器;Apple 開發的 hooks;Live Activities;watchOS 執行時;SwiftUI 內部;RealityKit 的空間思維模型;SwiftData 結構紀律;Liquid Glass 模式;多平台出貨;平台矩陣;Vision 框架;Symbol Effects;Core ML 推論;Writing Tools API;我拒絕撰寫的內容。樞紐在 Apple 生態系列。如需更廣泛的「iOS 搭配 AI 代理」脈絡,請參閱 iOS 代理開發指南。
FAQ
Swift Testing 會完全取代 XCTest 嗎?
尚未。XCTest 仍承載 UI 自動化測試(XCUIApplication)和效能測試(XCTMetric);Swift Testing 不涵蓋這些領域。對於單元測試、整合測試、非同步測試和參數化測試,Swift Testing 是新程式碼的建議預設值。兩個框架可以在同一個目標中共存而不衝突。
#expect 和 XCTAssertEqual 之間的實際差異是什麼?
#expect 擷取完整的 Swift 表達式,並在失敗時回報比較的兩側(例如 user.score == calculator.compute(user) 失敗,因為 user.score = 80 而 calculator.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。
參考資料
-
Apple Developer:Swift Testing。Apple 對 Swift Testing 隨 Xcode 16+ 和 Swift 6 採用的概覽。 ↩
-
開源儲存庫:GitHub 上的 swiftlang/swift-testing。框架的原始碼,依 Apache License 2.0 與 Swift Project 標準授權提供。 ↩↩
-
Apple Developer Documentation:從 XCTest 遷移測試。Apple 的官方遷移指南,涵蓋並排共存以及 XCUIApplication / XCTMetric 例外。 ↩↩↩↩↩↩
-
Apple Developer Documentation:Swift Testing。
@Test、@Suite、#expect、#require和特徵詞彙的框架參考。 ↩↩ -
Apple Developer Documentation:Trait。定義測試特徵的協定,內建特徵包括
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)。 ↩↩ -
Apple Developer:Meet Swift Testing(WWDC 2024 議程 10179)和 Go further with Swift Testing(WWDC 2024 議程 10195)。涵蓋參數化測試和並行性的介紹議程。 ↩↩
-
WWDC 2025 新增功能的開源儲存庫證據:
Sources/Testing/Attachments/Attachment.swift用於自訂附件,Sources/Testing/ExitTests/用於#expect(processExitsWith:)退出測試巨集。兩者均首次出現在 WWDC 2025,並隨 Xcode 26+ 一起推出。 ↩↩