mcpField
A
axiom-testing-async

axiom-testing-async

github.com/charleswiltgen测试与质量1.2k176 次安装今天更新
代码仓库

关于 axiom-testing-async

axiom-testing-async 是使用 Swift Testing 测试 Swift 异步代码的指南,面向编写或迁移测试的开发者。内容涵盖回调、Actor 隔离、并行执行及从 XCTest 迁移的模式。

主要功能

  1. 01异步与抛错函数测试
  2. 02使用 confirmation 检查回调
  3. 03MainActor 测试模式
  4. 04超时与参数化测试
  5. 05并行执行和共享状态指导
  6. 06XCTest 迁移模式

适用场景

  1. 01测试 async/await 函数
  2. 02验证回调次数和事件
  3. 03测试 MainActor 隔离的视图模型
  4. 04将异步测试从 XCTest 迁移

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-testing-async

安装后在 Claude Code 中输入/axiom-testing-async确认已加载。

详细介绍

测试异步代码 — Swift Testing 测试模式

使用 Swift Testing 框架测试 async/await 代码的现代模式。

适用场景

✅ 适用于:

  • 为异步函数编写测试
  • 使用 Swift Testing 测试基于回调的 API
  • 将异步 XCTests 迁移到 Swift Testing
  • 测试隔离于 MainActor 的代码
  • 需要验证事件触发的次数是否符合预期

❌ 不适用于:

  • 仅使用 XCTest 的项目(使用 XCTestExpectation)
  • UI 自动化测试(使用 XCUITest)
  • 使用指标进行性能测试(使用 XCTest)

与 XCTest 的主要区别

XCTestSwift Testing
XCTestExpectationconfirmation { }
wait(for:timeout:)await confirmation
隐式 @MainActor显式 @MainActor
默认串行默认并行
XCTAssertEqual()#expect()
continueAfterFailure每个预期使用 #require

模式

模式 1:简单异步函数
@Test func fetchUser() async throws {
    let user = try await api.fetchUser(id: 1)
    #expect(user.name == "Alice")
}
模式 2:完成处理程序 → Continuation

适用于没有异步重载的 API:

@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)
}
模式 3:使用 confirmation 确认单次回调

当回调应恰好触发一次时:

@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()
    }
}
模式 4:使用 expectedCount 确认多次回调
@Test func delegateCalledMultipleTimes() async {
    await confirmation(expectedCount: 3) { confirm in
        delegate.onProgress = { progress in
            confirm()  // Called 3 times
        }
        startDownload()  // Triggers 3 progress updates
    }
}
模式 5:验证回调绝不会触发
@Test func noErrorCallback() async {
    await confirmation(expectedCount: 0) { confirm in
        delegate.onError = { _ in
            confirm()  // Should never be called
        }
        performSuccessfulOperation()
    }
}
模式 6:MainActor 测试
@Test @MainActor func viewModelUpdates() async {
    let vm = ViewModel()
    await vm.load()
    #expect(vm.items.count > 0)
    #expect(vm.isLoading == false)
}
模式 7:控制超时
@Test(.timeLimit(.seconds(5)))
func slowOperation() async throws {
    try await longRunningTask()
}
模式 8:测试抛出错误
@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)
    }
}
模式 9:使用 #require 解包可选值
@Test func firstVideo() async throws {
    let videos = try await videoLibrary.videos()
    let first = try #require(videos.first)  // Fails if nil
    #expect(first.duration > 0)
}
模式 10:参数化异步测试
@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)
}

参数会自动并行运行。

并行测试执行

Swift Testing 默认并行运行测试(与 XCTest 不同)。

处理共享状态
// ❌ 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)
    }
}
强制串行执行

当测试必须按顺序运行时:

@Suite("Database tests", .serialized)
struct DatabaseTests {
    @Test func createRecord() async { /* ... */ }
    @Test func readRecord() async { /* ... */ }  // After create
    @Test func deleteRecord() async { /* ... */ }  // After read
}

注意:其他无关测试仍会并行运行。

常见错误

错误 1:使用 sleep 代替 confirmation
// ❌ 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()
    }
}
错误 2:忘记在 UI 测试中使用 @MainActor
// ❌ 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()
}
错误 3:回调缺少 confirmation
// ❌ 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()
        }
    }
}
错误 4:未处理并行执行
// ❌ 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")
}

从 XCTest 迁移

XCTestExpectation → confirmation
// 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()
        }
    }
}
异步 setUp → Suite init
// 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
    }
}

资源

WWDC:2024-10179,2024-10195

文档:/testing、/testing/confirmation

技能:axiom-swift-testing、axiom-ios-testing

评论

支持 Markdown