axiom-swift-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwift Testing
Swift Testing
Overview
概述
Swift Testing is Apple's modern testing framework introduced at WWDC 2024. It uses Swift macros (, ) instead of naming conventions, runs tests in parallel by default, and integrates seamlessly with Swift concurrency.
@Test#expectCore principle: Tests should be fast, reliable, and expressive. The fastest tests run without launching your app or simulator.
Swift Testing是苹果在WWDC 2024上推出的现代测试框架。它使用Swift宏(、)替代命名约定,默认并行运行测试,并与Swift并发无缝集成。
@Test#expect核心原则:测试应快速、可靠且具表达性。最快的测试无需启动应用或模拟器即可运行。
The Speed Hierarchy
速度层级
Tests run at dramatically different speeds depending on how they're configured:
| Configuration | Typical Time | Use Case |
|---|---|---|
| ~0.1s | Pure logic, models, algorithms |
| Host Application: None | ~3s | Framework code, no UI dependencies |
| Bypass app launch | ~6s | App target but skip initialization |
| Full app launch | 20-60s | UI tests, integration tests |
Key insight: Move testable logic into Swift Packages or frameworks, then test with or "None" host application.
swift test测试的运行速度因配置不同而有显著差异:
| 配置 | 典型耗时 | 使用场景 |
|---|---|---|
| ~0.1s | 纯逻辑、模型、算法 |
| 宿主应用:无 | ~3s | 框架代码,无UI依赖 |
| 跳过应用启动 | ~6s | 应用目标但跳过初始化 |
| 完整应用启动 | 20-60s | UI测试、集成测试 |
关键见解:将可测试逻辑迁移到Swift Package或框架中,然后使用或“无”宿主应用进行测试。
swift testBuilding Blocks
核心组件
@Test Functions
@Test 函数
swift
import Testing
@Test func videoHasCorrectMetadata() {
let video = Video(named: "example.mp4")
#expect(video.duration == 120)
}Key differences from XCTest:
- No prefix required —
testattribute is explicit@Test - Can be global functions, not just methods in a class
- Supports ,
async, and actor isolationthrows - Each test runs on a fresh instance of its containing suite
swift
import Testing
@Test func videoHasCorrectMetadata() {
let video = Video(named: "example.mp4")
#expect(video.duration == 120)
}与XCTest的主要区别:
- 无需前缀——
test属性是显式的@Test - 可以是全局函数,不局限于类中的方法
- 支持、
async和actor隔离throws - 每个测试都会在其所属套件的新实例上运行
#expect and #require
#expect 和 #require
swift
// Basic expectation — test continues on failure
#expect(result == expected)
#expect(array.isEmpty)
#expect(numbers.contains(42))
// Required expectation — test stops on failure
let user = try #require(await fetchUser(id: 123))
#expect(user.name == "Alice")
// Unwrap optionals safely
let first = try #require(items.first)
#expect(first.isValid)Why #expect is better than XCTAssert:
- Captures source code and sub-values automatically
- Single macro handles all operators (==, >, contains, etc.)
- No need for specialized assertions (XCTAssertEqual, XCTAssertNil, etc.)
swift
// 基础断言——失败后测试继续
#expect(result == expected)
#expect(array.isEmpty)
#expect(numbers.contains(42))
// 必要断言——失败后测试停止
let user = try #require(await fetchUser(id: 123))
#expect(user.name == "Alice")
// 安全解包可选值
let first = try #require(items.first)
#expect(first.isValid)#expect 优于 XCTAssert 的原因:
- 自动捕获源代码和子值
- 单个宏支持所有运算符(==、>、contains等)
- 无需专用断言(XCTAssertEqual、XCTAssertNil等)
Error Testing
错误测试
swift
// Expect any error
#expect(throws: (any Error).self) {
try dangerousOperation()
}
// Expect specific error type
#expect(throws: NetworkError.self) {
try fetchData()
}
// Expect specific error value
#expect(throws: ValidationError.invalidEmail) {
try validate(email: "not-an-email")
}
// Custom validation
#expect {
try process(data)
} throws: { error in
guard let networkError = error as? NetworkError else { return false }
return networkError.statusCode == 404
}swift
// 断言会抛出任意错误
#expect(throws: (any Error).self) {
try dangerousOperation()
}
// 断言会抛出特定错误类型
#expect(throws: NetworkError.self) {
try fetchData()
}
// 断言会抛出特定错误值
#expect(throws: ValidationError.invalidEmail) {
try validate(email: "not-an-email")
}
// 自定义验证
#expect {
try process(data)
} throws: { error in
guard let networkError = error as? NetworkError else { return false }
return networkError.statusCode == 404
}@Suite Types
@Suite 类型
swift
@Suite("Video Processing Tests")
struct VideoTests {
let video = Video(named: "sample.mp4") // Fresh instance per test
@Test func hasCorrectDuration() {
#expect(video.duration == 120)
}
@Test func hasCorrectResolution() {
#expect(video.resolution == CGSize(width: 1920, height: 1080))
}
}Key behaviors:
- Structs preferred (value semantics, no accidental state sharing)
- Each gets its own suite instance
@Test - Use for setup,
initfor teardown (actors/classes only)deinit - Nested suites supported for organization
swift
@Suite("Video Processing Tests")
struct VideoTests {
let video = Video(named: "sample.mp4") // 每个测试使用新实例
@Test func hasCorrectDuration() {
#expect(video.duration == 120)
}
@Test func hasCorrectResolution() {
#expect(video.resolution == CGSize(width: 1920, height: 1080))
}
}核心特性:
- 优先使用结构体(值语义,避免意外的状态共享)
- 每个都会获取自己的套件实例
@Test - 使用进行初始化,
init进行清理(仅适用于actor/类)deinit - 支持嵌套套件用于组织测试
Traits
特性
Traits customize test behavior:
swift
// Display name
@Test("User can log in with valid credentials")
func loginWithValidCredentials() { }
// Disable with reason
@Test(.disabled("Waiting for backend fix"))
func brokenFeature() { }
// Conditional execution
@Test(.enabled(if: FeatureFlags.newUIEnabled))
func newUITest() { }
// Time limit
@Test(.timeLimit(.minutes(1)))
func longRunningTest() async { }
// Bug reference
@Test(.bug("https://github.com/org/repo/issues/123", "Flaky on CI"))
func sometimesFailingTest() { }
// OS version requirement
@available(iOS 18, *)
@Test func iOS18OnlyFeature() { }特性可自定义测试行为:
swift
// 显示名称
@Test("User can log in with valid credentials")
func loginWithValidCredentials() { }
// 禁用并说明原因
@Test(.disabled("Waiting for backend fix"))
func brokenFeature() { }
// 条件执行
@Test(.enabled(if: FeatureFlags.newUIEnabled))
func newUITest() { }
// 时间限制
@Test(.timeLimit(.minutes(1)))
func longRunningTest() async { }
// Bug 引用
@Test(.bug("https://github.com/org/repo/issues/123", "Flaky on CI"))
func sometimesFailingTest() { }
// 操作系统版本要求
@available(iOS 18, *)
@Test func iOS18OnlyFeature() { }Tags for Organization
用于组织的标签
swift
// Define tags
extension Tag {
@Tag static var networking: Self
@Tag static var performance: Self
@Tag static var slow: Self
}
// Apply to tests
@Test(.tags(.networking, .slow))
func networkIntegrationTest() async { }
// Apply to entire suite
@Suite(.tags(.performance))
struct PerformanceTests {
@Test func benchmarkSort() { } // Inherits .performance tag
}Use tags to:
- Run subsets of tests (filter by tag in Test Navigator)
- Exclude slow tests from quick feedback loops
- Group related tests across different files/suites
swift
// 定义标签
extension Tag {
@Tag static var networking: Self
@Tag static var performance: Self
@Tag static var slow: Self
}
// 应用到测试
@Test(.tags(.networking, .slow))
func networkIntegrationTest() async { }
// 应用到整个套件
@Suite(.tags(.performance))
struct PerformanceTests {
@Test func benchmarkSort() { } // 继承.performance标签
}标签用途:
- 运行测试子集(在测试导航器中按标签筛选)
- 在快速反馈循环中排除慢速测试
- 跨不同文件/套件分组相关测试
Parameterized Testing
参数化测试
Transform repetitive tests into a single parameterized test:
swift
// ❌ Before: Repetitive
@Test func vanillaHasNoNuts() {
#expect(!IceCream.vanilla.containsNuts)
}
@Test func chocolateHasNoNuts() {
#expect(!IceCream.chocolate.containsNuts)
}
@Test func almondHasNuts() {
#expect(IceCream.almond.containsNuts)
}
// ✅ After: Parameterized
@Test(arguments: [IceCream.vanilla, .chocolate, .strawberry])
func flavorWithoutNuts(_ flavor: IceCream) {
#expect(!flavor.containsNuts)
}
@Test(arguments: [IceCream.almond, .pistachio])
func flavorWithNuts(_ flavor: IceCream) {
#expect(flavor.containsNuts)
}将重复测试转换为单个参数化测试:
swift
// ❌ 之前:重复代码
@Test func vanillaHasNoNuts() {
#expect(!IceCream.vanilla.containsNuts)
}
@Test func chocolateHasNoNuts() {
#expect(!IceCream.chocolate.containsNuts)
}
@Test func almondHasNuts() {
#expect(IceCream.almond.containsNuts)
}
// ✅ 之后:参数化
@Test(arguments: [IceCream.vanilla, .chocolate, .strawberry])
func flavorWithoutNuts(_ flavor: IceCream) {
#expect(!flavor.containsNuts)
}
@Test(arguments: [IceCream.almond, .pistachio])
func flavorWithNuts(_ flavor: IceCream) {
#expect(flavor.containsNuts)
}Two-Collection Parameterization
双集合参数化
swift
// Test all combinations (4 × 3 = 12 test cases)
@Test(arguments: [1, 2, 3, 4], ["a", "b", "c"])
func allCombinations(number: Int, letter: String) {
// Tests: (1,"a"), (1,"b"), (1,"c"), (2,"a"), ...
}
// Test paired values only (3 test cases)
@Test(arguments: zip([1, 2, 3], ["one", "two", "three"]))
func pairedValues(number: Int, name: String) {
// Tests: (1,"one"), (2,"two"), (3,"three")
}swift
// 测试所有组合(4 × 3 = 12个测试用例)
@Test(arguments: [1, 2, 3, 4], ["a", "b", "c"])
func allCombinations(number: Int, letter: String) {
// 测试用例:(1,"a"), (1,"b"), (1,"c"), (2,"a"), ...
}
// 仅测试配对值(3个测试用例)
@Test(arguments: zip([1, 2, 3], ["one", "two", "three"]))
func pairedValues(number: Int, name: String) {
// 测试用例:(1,"one"), (2,"two"), (3,"three")
}Benefits Over For-Loops
优于for循环的优势
| For-Loop | Parameterized |
|---|---|
| Stops on first failure | All arguments run |
| Unclear which value failed | Each argument shown separately |
| Sequential execution | Parallel execution |
| Can't re-run single case | Re-run individual arguments |
| For循环 | 参数化测试 |
|---|---|
| 首次失败后停止 | 运行所有参数 |
| 无法明确哪个值导致失败 | 每个参数的结果单独显示 |
| 顺序执行 | 并行执行 |
| 无法重新运行单个用例 | 可重新运行单个参数用例 |
Fast Tests: Architecture for Testability
快速测试:为可测试性构建架构
Strategy 1: Swift Package for Logic (Fastest)
策略1:将逻辑放入Swift Package(最快)
Move pure logic into a Swift Package:
MyApp/
├── MyApp/ # App target (UI, app lifecycle)
├── MyAppCore/ # Swift Package (testable logic)
│ ├── Package.swift
│ └── Sources/
│ └── MyAppCore/
│ ├── Models/
│ ├── Services/
│ └── Utilities/
└── MyAppCoreTests/ # Package testsRun with — no simulator, no app launch:
swift testbash
cd MyAppCore
swift test # Runs in ~0.1 seconds将纯逻辑迁移到Swift Package:
MyApp/
├── MyApp/ # 应用目标(UI、应用生命周期)
├── MyAppCore/ # Swift Package(可测试逻辑)
│ ├── Package.swift
│ └── Sources/
│ └── MyAppCore/
│ ├── Models/
│ ├── Services/
│ └── Utilities/
└── MyAppCoreTests/ # Package测试使用运行——无需模拟器,无需启动应用:
swift testbash
cd MyAppCore
swift test # 约0.1秒完成Strategy 2: Framework with No Host Application
策略2:使用无宿主应用的框架
For code that must stay in the app project:
- Create a framework target (File → New → Target → Framework)
- Move model code into the framework
- Make types public that need external access
- Add imports in files using the framework
- Set Host Application to "None" in test target settings
Project Settings → Test Target → Testing
Host Application: None ← Key setting
☐ Allow testing Host Application APIsBuild+test time: ~3 seconds vs 20-60 seconds with app launch.
对于必须保留在应用项目中的代码:
- 创建框架目标(文件 → 新建 → 目标 → 框架)
- 将模型代码迁移到框架
- 将需要外部访问的类型设为public
- 在使用框架的文件中添加导入语句
- 在测试目标设置中将宿主应用设为“无”
项目设置 → 测试目标 → 测试
宿主应用:无 ← 关键设置
☐ 允许测试宿主应用API构建+测试耗时:约3秒,相比启动应用的20-60秒大幅缩短。
Strategy 3: Bypass SwiftUI App Launch
策略3:绕过SwiftUI应用启动
If you can't use a framework, bypass the app launch:
swift
// Simple solution (no custom startup code)
@main
struct ProductionApp: App {
var body: some Scene {
WindowGroup {
if !isRunningTests {
ContentView()
}
}
}
private var isRunningTests: Bool {
NSClassFromString("XCTestCase") != nil
}
}swift
// Thorough solution (custom startup code)
@main
struct MainEntryPoint {
static func main() {
if NSClassFromString("XCTestCase") != nil {
TestApp.main() // Empty app for tests
} else {
ProductionApp.main()
}
}
}
struct TestApp: App {
var body: some Scene {
WindowGroup { } // Empty
}
}如果无法使用框架,可绕过应用启动:
swift
// 简单方案(无自定义启动代码)
@main
struct ProductionApp: App {
var body: some Scene {
WindowGroup {
if !isRunningTests {
ContentView()
}
}
}
private var isRunningTests: Bool {
NSClassFromString("XCTestCase") != nil
}
}swift
// 完整方案(自定义启动代码)
@main
struct MainEntryPoint {
static func main() {
if NSClassFromString("XCTestCase") != nil {
TestApp.main() // 测试用空应用
} else {
ProductionApp.main()
}
}
}
struct TestApp: App {
var body: some Scene {
WindowGroup { } // 空界面
}
}Async Testing
异步测试
Basic Async Tests
基础异步测试
swift
@Test func fetchUserReturnsData() async throws {
let user = try await userService.fetch(id: 123)
#expect(user.name == "Alice")
}swift
@Test func fetchUserReturnsData() async throws {
let user = try await userService.fetch(id: 123)
#expect(user.name == "Alice")
}Testing Callbacks with Continuations
使用Continuations测试回调
swift
// Convert completion handler to async
@Test func legacyAPIWorks() async throws {
let result = try await withCheckedThrowingContinuation { continuation in
legacyService.fetchData { result in
continuation.resume(with: result)
}
}
#expect(result.count > 0)
}swift
// 将完成处理程序转换为异步
@Test func legacyAPIWorks() async throws {
let result = try await withCheckedThrowingContinuation { continuation in
legacyService.fetchData { result in
continuation.resume(with: result)
}
}
#expect(result.count > 0)
}Confirmations for Multiple Events
多事件确认
swift
@Test func cookiesAreEaten() async {
await confirmation("cookie eaten", expectedCount: 10) { confirm in
let jar = CookieJar(count: 10)
jar.onCookieEaten = { confirm() }
await jar.eatAll()
}
}
// Confirm something never happens
await confirmation(expectedCount: 0) { confirm in
let cache = Cache()
cache.onEviction = { confirm() }
cache.store("small-item") // Should not trigger eviction
}swift
@Test func cookiesAreEaten() async {
await confirmation("cookie eaten", expectedCount: 10) { confirm in
let jar = CookieJar(count: 10)
jar.onCookieEaten = { confirm() }
await jar.eatAll()
}
}
// 确认某事件从未发生
await confirmation(expectedCount: 0) { confirm in
let cache = Cache()
cache.onEviction = { confirm() }
cache.store("small-item") // 不应触发驱逐
}Reliable Async Testing with Concurrency Extras
使用并发工具实现可靠的异步测试
Problem: Async tests can be flaky due to scheduling unpredictability.
swift
// ❌ Flaky: Task scheduling is unpredictable
@Test func loadingStateChanges() async {
let model = ViewModel()
let task = Task { await model.loadData() }
#expect(model.isLoading == true) // Often fails!
await task.value
}Solution: Use Point-Free's :
swift-concurrency-extrasswift
import ConcurrencyExtras
@Test func loadingStateChanges() async {
await withMainSerialExecutor {
let model = ViewModel()
let task = Task { await model.loadData() }
await Task.yield()
#expect(model.isLoading == true) // Deterministic!
await task.value
#expect(model.isLoading == false)
}
}Why it works: Serializes async work to main thread, making suspension points deterministic.
问题:由于调度不可预测,异步测试可能不稳定。
swift
// ❌ 不稳定:任务调度不可预测
@Test func loadingStateChanges() async {
let model = ViewModel()
let task = Task { await model.loadData() }
#expect(model.isLoading == true) // 经常失败!
await task.value
}解决方案:使用Point-Free的:
swift-concurrency-extrasswift
import ConcurrencyExtras
@Test func loadingStateChanges() async {
await withMainSerialExecutor {
let model = ViewModel()
let task = Task { await model.loadData() }
await Task.yield()
#expect(model.isLoading == true) // 确定执行!
await task.value
#expect(model.isLoading == false)
}
}原理:将异步工作序列化到主线程,使挂起点可预测。
Deterministic Time with TestClock
使用TestClock实现确定的时间控制
Use Point-Free's to control time in tests:
swift-clocksswift
import Clocks
@MainActor
class FeatureModel: ObservableObject {
@Published var count = 0
let clock: any Clock<Duration>
var timerTask: Task<Void, Error>?
init(clock: any Clock<Duration>) {
self.clock = clock
}
func startTimer() {
timerTask = Task {
while true {
try await clock.sleep(for: .seconds(1))
count += 1
}
}
}
}
// Test with controlled time
@Test func timerIncrements() async {
let clock = TestClock()
let model = FeatureModel(clock: clock)
model.startTimer()
await clock.advance(by: .seconds(1))
#expect(model.count == 1)
await clock.advance(by: .seconds(4))
#expect(model.count == 5)
model.timerTask?.cancel()
}Clock types:
- — Advance time manually, deterministic
TestClock - — All sleeps return instantly (great for previews)
ImmediateClock - — Fails if used (catch unexpected time dependencies)
UnimplementedClock
使用Point-Free的在测试中控制时间:
swift-clocksswift
import Clocks
@MainActor
class FeatureModel: ObservableObject {
@Published var count = 0
let clock: any Clock<Duration>
var timerTask: Task<Void, Error>?
init(clock: any Clock<Duration>) {
self.clock = clock
}
func startTimer() {
timerTask = Task {
while true {
try await clock.sleep(for: .seconds(1))
count += 1
}
}
}
}
// 使用受控时间进行测试
@Test func timerIncrements() async {
let clock = TestClock()
let model = FeatureModel(clock: clock)
model.startTimer()
await clock.advance(by: .seconds(1))
#expect(model.count == 1)
await clock.advance(by: .seconds(4))
#expect(model.count == 5)
model.timerTask?.cancel()
}时钟类型:
- — 手动推进时间,执行确定
TestClock - — 所有sleep立即返回(非常适合预览)
ImmediateClock - — 使用时会失败(捕获意外的时间依赖)
UnimplementedClock
Parallel Testing
并行测试
Swift Testing runs tests in parallel by default.
Swift Testing默认并行运行测试。
When to Serialize
何时序列化执行
swift
// Serialize tests in a suite that share external state
@Suite(.serialized)
struct DatabaseTests {
@Test func createUser() { }
@Test func deleteUser() { } // Runs after createUser
}
// Serialize parameterized test cases
@Test(.serialized, arguments: [1, 2, 3])
func sequentialProcessing(value: Int) { }swift
// 序列化共享外部状态的套件中的测试
@Suite(.serialized)
struct DatabaseTests {
@Test func createUser() { }
@Test func deleteUser() { } // 在createUser之后运行
}
// 序列化参数化测试用例
@Test(.serialized, arguments: [1, 2, 3])
func sequentialProcessing(value: Int) { }Hidden Dependencies
隐藏依赖
swift
// ❌ Bug: Tests depend on execution order
@Suite struct CookieTests {
static var cookie: Cookie?
@Test func bakeCookie() {
Self.cookie = Cookie() // Sets shared state
}
@Test func eatCookie() {
#expect(Self.cookie != nil) // Fails if runs first!
}
}
// ✅ Fixed: Each test is independent
@Suite struct CookieTests {
@Test func bakeCookie() {
let cookie = Cookie()
#expect(cookie.isBaked)
}
@Test func eatCookie() {
let cookie = Cookie()
cookie.eat()
#expect(cookie.isEaten)
}
}Random order helps expose these bugs — fix them rather than serialize.
swift
// ❌ 错误:测试依赖执行顺序
@Suite struct CookieTests {
static var cookie: Cookie?
@Test func bakeCookie() {
Self.cookie = Cookie() // 设置共享状态
}
@Test func eatCookie() {
#expect(Self.cookie != nil) // 如果先运行则失败!
}
}
// ✅ 修复:每个测试独立
@Suite struct CookieTests {
@Test func bakeCookie() {
let cookie = Cookie()
#expect(cookie.isBaked)
}
@Test func eatCookie() {
let cookie = Cookie()
cookie.eat()
#expect(cookie.isEaten)
}
}随机顺序有助于暴露此类错误——应修复错误而非序列化执行。
Known Issues
已知问题
Handle expected failures without noise:
swift
@Test func featureUnderDevelopment() {
withKnownIssue("Backend not ready yet") {
try callUnfinishedAPI()
}
}
// Conditional known issue
@Test func platformSpecificBug() {
withKnownIssue("Fails on iOS 17.0") {
try reproduceEdgeCaseBug()
} when: {
ProcessInfo().operatingSystemVersion.majorVersion == 17
}
}Better than .disabled because:
- Test still compiles (catches syntax errors)
- You're notified when the issue is fixed
- Results show "expected failure" not "skipped"
处理预期失败而不产生冗余信息:
swift
@Test func featureUnderDevelopment() {
withKnownIssue("Backend not ready yet") {
try callUnfinishedAPI()
}
}
// 条件性已知问题
@Test func platformSpecificBug() {
withKnownIssue("Fails on iOS 17.0") {
try reproduceEdgeCaseBug()
} when: {
ProcessInfo().operatingSystemVersion.majorVersion == 17
}
}比.disabled更好的原因:
- 测试仍会编译(捕获语法错误)
- 问题修复时会收到通知
- 结果显示“预期失败”而非“已跳过”
Migration from XCTest
从XCTest迁移
Comparison Table
对比表
| XCTest | Swift Testing |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| XCTest | Swift Testing |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
Keep Using XCTest For
仍需使用XCTest的场景
- UI tests (XCUIApplication)
- Performance tests (XCTMetric)
- Objective-C tests
- UI测试(XCUIApplication)
- 性能测试(XCTMetric)
- Objective-C测试
Migration Tips
迁移技巧
- Both frameworks can coexist in the same target
- Migrate incrementally, one test file at a time
- Consolidate similar XCTests into parameterized Swift tests
- Single-test XCTestCase → global function
@Test
- 两个框架可在同一目标中共存
- 逐步迁移,一次迁移一个测试文件
- 将相似的XCTest合并为参数化Swift测试
- 单个测试的XCTestCase → 全局函数
@Test
Common Mistakes
常见错误
❌ Mixing Assertions
❌ 混合使用断言
swift
// Don't mix XCTest and Swift Testing
@Test func badExample() {
XCTAssertEqual(1, 1) // ❌ Wrong framework
#expect(1 == 1) // ✅ Use this
}swift
// 不要混合XCTest和Swift Testing
@Test func badExample() {
XCTAssertEqual(1, 1) // ❌ 错误的框架
#expect(1 == 1) // ✅ 使用这个
}❌ Using Classes for Suites
❌ 使用类作为套件
swift
// ❌ Avoid: Reference semantics can cause shared state bugs
@Suite class VideoTests { }
// ✅ Prefer: Value semantics isolate each test
@Suite struct VideoTests { }swift
// ❌ 避免:引用语义可能导致状态共享错误
@Suite class VideoTests { }
// ✅ 推荐:值语义隔离每个测试
@Suite struct VideoTests { }❌ Forgetting @MainActor
❌ 忘记@MainActor
swift
// ❌ May fail with Swift 6 strict concurrency
@Test func updateUI() async {
viewModel.updateTitle("New") // Data race warning
}
// ✅ Isolate to main actor
@Test @MainActor func updateUI() async {
viewModel.updateTitle("New")
}swift
// ❌ 在Swift 6严格并发模式下可能失败
@Test func updateUI() async {
viewModel.updateTitle("New") // 数据竞争警告
}
// ✅ 隔离到主线程actor
@Test @MainActor func updateUI() async {
viewModel.updateTitle("New")
}❌ Over-Serializing
❌ 过度序列化
swift
// ❌ Don't serialize just because tests use async
@Suite(.serialized) struct APITests { } // Defeats parallelism
// ✅ Only serialize when tests truly share mutable stateswift
// ❌ 不要仅因为测试使用异步就序列化
@Suite(.serialized) struct APITests { } // 失去并行优势
// ✅ 仅当测试确实共享可变状态时才序列化❌ XCTestCase with Swift 6.2 MainActor Default
❌ Swift 6.2默认MainActor下的XCTestCase
Swift 6.2's breaks XCTestCase:
default-actor-isolation = MainActorswift
// ❌ Error: Main actor-isolated initializer 'init()' has different
// actor isolation from nonisolated overridden declaration
final class PlaygroundTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()
}
}Solution: Mark XCTestCase subclass as :
nonisolatedswift
// ✅ Works with MainActor default isolation
nonisolated final class PlaygroundTests: XCTestCase {
@MainActor
override func setUp() async throws {
try await super.setUp()
}
@Test @MainActor
func testSomething() async {
// Individual tests can be @MainActor
}
}Why: XCTestCase is Objective-C, not annotated for Swift concurrency. Its initializers are , causing conflicts with MainActor-isolated subclasses.
nonisolatedBetter solution: Migrate to Swift Testing () which handles isolation properly.
@Suite structSwift 6.2的会破坏XCTestCase:
default-actor-isolation = MainActorswift
// ❌ 错误:主线程actor隔离的初始化器'init()'与非隔离的重写声明的actor隔离不同
final class PlaygroundTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()
}
}解决方案:将XCTestCase子类标记为:
nonisolatedswift
// ✅ 在默认MainActor隔离下正常工作
nonisolated final class PlaygroundTests: XCTestCase {
@MainActor
override func setUp() async throws {
try await super.setUp()
}
@Test @MainActor
func testSomething() async {
// 单个测试可以标记为@MainActor
}
}原因:XCTestCase是Objective-C实现,未针对Swift并发进行注解。其初始化器是,与MainActor隔离的子类冲突。
nonisolated更好的解决方案:迁移到Swift Testing(),它能正确处理隔离。
@Suite structXcode Optimization for Fast Feedback
Xcode优化以实现快速反馈
Turn Off Parallel XCTest Execution
关闭XCTest并行执行
Swift Testing runs in parallel by default; XCTest parallelization adds overhead:
Test Plan → Options → Parallelization → "Swift Testing Only"Swift Testing默认并行运行;XCTest并行化会增加开销:
测试计划 → 选项 → 并行化 → "仅Swift Testing"Turn Off Test Debugger
关闭测试调试器
Attaching the debugger costs ~1 second per run:
Scheme → Edit Scheme → Test → Info → ☐ Debugger附加调试器每次运行约耗时1秒:
方案 → 编辑方案 → 测试 → 信息 → ☐ 调试器Delete UI Test Templates
删除UI测试模板
Xcode's default UI tests slow everything down. Remove them:
- Delete UI test target (Project Settings → select target → -)
- Delete UI test source folder
Xcode默认的UI测试会拖慢所有流程。删除它们:
- 删除UI测试目标(项目设置 → 选择目标 → -)
- 删除UI测试源文件夹
Disable dSYM for Debug Builds
禁用Debug构建的dSYM
Build Settings → Debug Information Format
Debug: DWARF
Release: DWARF with dSYM File构建设置 → 调试信息格式
Debug: DWARF
Release: DWARF with dSYM FileCheck Build Scripts
检查构建脚本
Run Script phases without defined inputs/outputs cause full rebuilds. Always specify:
- Input Files / Input File Lists
- Output Files / Output File Lists
未定义输入/输出的Run Script阶段会导致完整重建。始终指定:
- 输入文件 / 输入文件列表
- 输出文件 / 输出文件列表
Checklist
检查清单
Before Writing Tests
编写测试前
- Identify what can move to a Swift Package (pure logic)
- Set up framework target if package isn't viable
- Configure Host Application: None for unit tests
- 确定可迁移到Swift Package的内容(纯逻辑)
- 如果无法使用Package,设置框架目标
- 为单元测试配置宿主应用:无
Writing Tests
编写测试时
- Use with clear display names
@Test - Use for all assertions
#expect - Use to fail fast on preconditions
#require - Use parameterization for similar test cases
- Add for organization
.tags()
- 使用并设置清晰的显示名称
@Test - 所有断言使用
#expect - 使用在前置条件失败时快速终止
#require - 对相似测试用例使用参数化
- 添加用于组织
.tags()
Async Tests
异步测试
- Mark test functions and use
asyncawait - Use for callback-based code
confirmation() - Consider for flaky tests
withMainSerialExecutor
- 标记测试函数为并使用
asyncawait - 对基于回调的代码使用
confirmation() - 对不稳定的测试考虑使用
withMainSerialExecutor
Parallel Safety
并行安全性
- Avoid shared mutable state between tests
- Use fresh instances in each test
- Only use when absolutely necessary
.serialized
- 避免测试之间共享可变状态
- 在每个测试中使用新实例
- 仅在绝对必要时使用
.serialized
Resources
资源
WWDC: 2024-10179, 2024-10195
Docs: /testing, /testing/migratingfromxctest, /testing/testing-asynchronous-code, /testing/parallelization
GitHub: pointfreeco/swift-concurrency-extras, pointfreeco/swift-clocks
History: See git log for changes
WWDC:2024-10179, 2024-10195
文档:/testing, /testing/migratingfromxctest, /testing/testing-asynchronous-code, /testing/parallelization
GitHub:pointfreeco/swift-concurrency-extras, pointfreeco/swift-clocks
历史记录:查看git log了解变更