axiom-testing-async
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Async Code — Swift Testing Patterns
异步代码测试——Swift Testing 模式
Modern patterns for testing async/await code with Swift Testing framework.
使用Swift Testing框架测试async/await代码的现代模式。
When to Use
适用场景
✅ Use when:
- Writing tests for async functions
- Testing callback-based APIs with Swift Testing
- Migrating async XCTests to Swift Testing
- Testing MainActor-isolated code
- Need to verify events fire expected number of times
❌ Don't use when:
- XCTest-only project (use XCTestExpectation)
- UI automation tests (use XCUITest)
- Performance testing with metrics (use XCTest)
✅ 适用场景:
- 为异步函数编写测试
- 使用Swift Testing测试基于回调的API
- 将异步XCTest迁移至Swift Testing
- 测试MainActor隔离的代码
- 需要验证事件触发次数符合预期
❌ 不适用场景:
- 仅使用XCTest的项目(使用XCTestExpectation)
- UI自动化测试(使用XCUITest)
- 带指标的性能测试(使用XCTest)
Key Differences from XCTest
与XCTest的主要区别
| XCTest | Swift Testing |
|---|---|
| |
| |
| |
| Serial by default | Parallel by default |
| |
| |
| XCTest | Swift Testing |
|---|---|
| |
| |
隐式 | 显式 |
| 默认串行执行 | 默认并行执行 |
| |
| 每个预期使用 |
Patterns
测试模式
Pattern 1: Simple Async Function
模式1:简单异步函数
swift
@Test func fetchUser() async throws {
let user = try await api.fetchUser(id: 1)
#expect(user.name == "Alice")
}swift
@Test func fetchUser() async throws {
let user = try await api.fetchUser(id: 1)
#expect(user.name == "Alice")
}Pattern 2: Completion Handler → Continuation
模式2:完成处理器→Continuation
For APIs without async overloads:
swift
@Test func legacyAPI() async throws {
let result = try await withCheckedThrowingContinuation { continuation in
legacyFetch { result, error in
if let result {
continuation.resume(returning: result)
} else {
continuation.resume(throwing: error!)
}
}
}
#expect(result.isValid)
}针对没有异步重载的API:
swift
@Test func legacyAPI() async throws {
let result = try await withCheckedThrowingContinuation { continuation in
legacyFetch { result, error in
if let result {
continuation.resume(returning: result)
} else {
continuation.resume(throwing: error!)
}
}
}
#expect(result.isValid)
}Pattern 3: Single Callback with confirmation
模式3:带confirmation的单次回调
When a callback should fire exactly once:
swift
@Test func notificationFires() async {
await confirmation { confirm in
NotificationCenter.default.addObserver(
forName: .didUpdate,
object: nil,
queue: .main
) { _ in
confirm() // Must be called exactly once
}
triggerUpdate()
}
}当回调应恰好触发一次时:
swift
@Test func notificationFires() async {
await confirmation { confirm in
NotificationCenter.default.addObserver(
forName: .didUpdate,
object: nil,
queue: .main
) { _ in
confirm() // 必须恰好调用一次
}
triggerUpdate()
}
}Pattern 4: Multiple Callbacks with expectedCount
模式4:带expectedCount的多次回调
swift
@Test func delegateCalledMultipleTimes() async {
await confirmation(expectedCount: 3) { confirm in
delegate.onProgress = { progress in
confirm() // Called 3 times
}
startDownload() // Triggers 3 progress updates
}
}swift
@Test func delegateCalledMultipleTimes() async {
await confirmation(expectedCount: 3) { confirm in
delegate.onProgress = { progress in
confirm() // 调用3次
}
startDownload() // 触发3次进度更新
}
}Pattern 5: Verify Callback Never Fires
模式5:验证回调从不触发
swift
@Test func noErrorCallback() async {
await confirmation(expectedCount: 0) { confirm in
delegate.onError = { _ in
confirm() // Should never be called
}
performSuccessfulOperation()
}
}swift
@Test func noErrorCallback() async {
await confirmation(expectedCount: 0) { confirm in
delegate.onError = { _ in
confirm() // 应从不被调用
}
performSuccessfulOperation()
}
}Pattern 6: MainActor Tests
模式6:MainActor测试
swift
@Test @MainActor func viewModelUpdates() async {
let vm = ViewModel()
await vm.load()
#expect(vm.items.count > 0)
#expect(vm.isLoading == false)
}swift
@Test @MainActor func viewModelUpdates() async {
let vm = ViewModel()
await vm.load()
#expect(vm.items.count > 0)
#expect(vm.isLoading == false)
}Pattern 7: Timeout Control
模式7:超时控制
swift
@Test(.timeLimit(.seconds(5)))
func slowOperation() async throws {
try await longRunningTask()
}swift
@Test(.timeLimit(.seconds(5)))
func slowOperation() async throws {
try await longRunningTask()
}Pattern 8: Testing Throws
模式8:测试抛出异常
swift
@Test func invalidInputThrows() async throws {
await #expect(throws: ValidationError.self) {
try await validate(input: "")
}
}
// Specific error
@Test func specificError() async throws {
await #expect(throws: NetworkError.notFound) {
try await api.fetch(id: -1)
}
}swift
@Test func invalidInputThrows() async throws {
await #expect(throws: ValidationError.self) {
try await validate(input: "")
}
}
// 特定异常
@Test func specificError() async throws {
await #expect(throws: NetworkError.notFound) {
try await api.fetch(id: -1)
}
}Pattern 9: Optional Unwrapping with #require
模式9:使用#require解包可选值
swift
@Test func firstVideo() async throws {
let videos = try await videoLibrary.videos()
let first = try #require(videos.first) // Fails if nil
#expect(first.duration > 0)
}swift
@Test func firstVideo() async throws {
let videos = try await videoLibrary.videos()
let first = try #require(videos.first) // 如果为nil则测试失败
#expect(first.duration > 0)
}Pattern 10: Parameterized Async Tests
模式10:参数化异步测试
swift
@Test("Video loading", arguments: [
"Beach.mov",
"Mountain.mov",
"City.mov"
])
func loadVideo(fileName: String) async throws {
let video = try await Video.load(fileName)
#expect(video.isPlayable)
}Arguments run in parallel automatically.
swift
@Test("Video loading", arguments: [
"Beach.mov",
"Mountain.mov",
"City.mov"
])
func loadVideo(fileName: String) async throws {
let video = try await Video.load(fileName)
#expect(video.isPlayable)
}参数会自动并行运行。
Parallel Test Execution
并行测试执行
Swift Testing runs tests in parallel by default (unlike XCTest).
Swift Testing默认并行运行测试(与XCTest不同)。
Handling Shared State
处理共享状态
swift
// ❌ Shared mutable state — race condition
var sharedCounter = 0
@Test func test1() async {
sharedCounter += 1 // Data race!
}
@Test func test2() async {
sharedCounter += 1 // Data race!
}
// ✅ Each test gets fresh instance
struct CounterTests {
var counter = Counter() // Fresh per test
@Test func increment() {
counter.increment()
#expect(counter.value == 1)
}
}swift
// ❌ 共享可变状态——竞态条件
var sharedCounter = 0
@Test func test1() async {
sharedCounter += 1 // 数据竞态!
}
@Test func test2() async {
sharedCounter += 1 // 数据竞态!
}
// ✅ 每个测试获取全新实例
struct CounterTests {
var counter = Counter() // 每个测试实例全新
@Test func increment() {
counter.increment()
#expect(counter.value == 1)
}
}Forcing Serial Execution
强制串行执行
When tests must run sequentially:
swift
@Suite("Database tests", .serialized)
struct DatabaseTests {
@Test func createRecord() async { /* ... */ }
@Test func readRecord() async { /* ... */ } // After create
@Test func deleteRecord() async { /* ... */ } // After read
}Note: Other unrelated tests still run in parallel.
当测试必须按顺序运行时:
swift
@Suite("Database tests", .serialized)
struct DatabaseTests {
@Test func createRecord() async { /* ... */ }
@Test func readRecord() async { /* ... */ } // 在create之后运行
@Test func deleteRecord() async { /* ... */ } // 在read之后运行
}注意:其他不相关的测试仍会并行运行。
Common Mistakes
常见错误
Mistake 1: Using sleep Instead of confirmation
错误1:使用sleep而非confirmation
swift
// ❌ Flaky — arbitrary wait time
@Test func eventFires() async {
setupEventHandler()
try await Task.sleep(for: .seconds(1)) // Hope it happened?
#expect(eventReceived)
}
// ✅ Deterministic — waits for actual event
@Test func eventFires() async {
await confirmation { confirm in
onEvent = { confirm() }
triggerEvent()
}
}swift
// ❌ 不稳定——等待时间随意
@Test func eventFires() async {
setupEventHandler()
try await Task.sleep(for: .seconds(1)) // 寄希望于事件已发生?
#expect(eventReceived)
}
// ✅ 确定性——等待实际事件触发
@Test func eventFires() async {
await confirmation { confirm in
onEvent = { confirm() }
triggerEvent()
}
}Mistake 2: Forgetting @MainActor on UI Tests
错误2:UI测试中遗漏@MainActor
swift
// ❌ Data race — ViewModel may be MainActor
@Test func viewModel() async {
let vm = ViewModel()
await vm.load() // May cause data race warnings
}
// ✅ Explicit isolation
@Test @MainActor func viewModel() async {
let vm = ViewModel()
await vm.load()
}swift
// ❌ 数据竞态——ViewModel可能属于MainActor
@Test func viewModel() async {
let vm = ViewModel()
await vm.load() // 可能引发数据竞态警告
}
// ✅ 显式隔离
@Test @MainActor func viewModel() async {
let vm = ViewModel()
await vm.load()
}Mistake 3: Missing confirmation for Callbacks
错误3:回调测试中遗漏confirmation
swift
// ❌ Test passes immediately — doesn't wait for callback
@Test func callback() async {
api.fetch { result in
#expect(result.isSuccess) // Never executed before test ends
}
}
// ✅ Waits for callback
@Test func callback() async {
await confirmation { confirm in
api.fetch { result in
#expect(result.isSuccess)
confirm()
}
}
}swift
// ❌ 测试立即通过——未等待回调
@Test func callback() async {
api.fetch { result in
#expect(result.isSuccess) // 测试结束前从未执行
}
}
// ✅ 等待回调
@Test func callback() async {
await confirmation { confirm in
api.fetch { result in
#expect(result.isSuccess)
confirm()
}
}
}Mistake 4: Not Handling Parallel Execution
错误4:未处理并行执行
swift
// ❌ Tests interfere with each other
@Test func writeFile() async {
try! "data".write(to: sharedFileURL, atomically: true, encoding: .utf8)
}
@Test func readFile() async {
let data = try! String(contentsOf: sharedFileURL) // May fail!
}
// ✅ Use unique files or .serialized
@Test func writeAndRead() async {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
try! "data".write(to: url, atomically: true, encoding: .utf8)
let data = try! String(contentsOf: url)
#expect(data == "data")
}swift
// ❌ 测试互相干扰
@Test func writeFile() async {
try! "data".write(to: sharedFileURL, atomically: true, encoding: .utf8)
}
@Test func readFile() async {
let data = try! String(contentsOf: sharedFileURL) // 可能失败!
}
// ✅ 使用唯一文件或.serialized
@Test func writeAndRead() async {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
try! "data".write(to: url, atomically: true, encoding: .utf8)
let data = try! String(contentsOf: url)
#expect(data == "data")
}Migration from XCTest
从XCTest迁移
XCTestExpectation → confirmation
XCTestExpectation → confirmation
swift
// XCTest
func testFetch() {
let expectation = expectation(description: "fetch")
api.fetch { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
// Swift Testing
@Test func fetch() async {
await confirmation { confirm in
api.fetch { result in
#expect(result != nil)
confirm()
}
}
}swift
// XCTest
func testFetch() {
let expectation = expectation(description: "fetch")
api.fetch { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
// Swift Testing
@Test func fetch() async {
await confirmation { confirm in
api.fetch { result in
#expect(result != nil)
confirm()
}
}
}Async setUp → Suite init
Async setUp → Suite初始化
swift
// XCTest
class MyTests: XCTestCase {
var service: Service!
override func setUp() async throws {
service = try await Service.create()
}
}
// Swift Testing
struct MyTests {
let service: Service
init() async throws {
service = try await Service.create()
}
@Test func example() async {
// Use self.service
}
}swift
// XCTest
class MyTests: XCTestCase {
var service: Service!
override func setUp() async throws {
service = try await Service.create()
}
}
// Swift Testing
struct MyTests {
let service: Service
init() async throws {
service = try await Service.create()
}
@Test func example() async {
// 使用self.service
}
}Resources
资源
WWDC: 2024-10179, 2024-10195
Docs: /testing, /testing/confirmation
Skills: axiom-swift-testing, axiom-ios-testing
WWDC: 2024-10179, 2024-10195
文档: /testing, /testing/confirmation
技能: axiom-swift-testing, axiom-ios-testing