swift-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Swift Testing

Swift Testing

Swift Testing is the modern testing framework for Swift (Xcode 16+, Swift 6+). Prefer it over XCTest for all new unit tests. Use XCTest only for UI tests, performance benchmarks, and snapshot tests.
Swift Testing是适用于Swift的现代化测试框架(要求Xcode 16+、Swift 6+)。所有新的单元测试都优先使用该框架,仅在UI测试、性能基准测试和快照测试中使用XCTest。

Basic Tests

基础测试

swift
import Testing

@Test("User can update their display name")
func updateDisplayName() {
    var user = User(name: "Alice")
    user.name = "Bob"
    #expect(user.name == "Bob")
}
swift
import Testing

@Test("User can update their display name")
func updateDisplayName() {
    var user = User(name: "Alice")
    user.name = "Bob"
    #expect(user.name == "Bob")
}

@Test Traits

@Test Traits

swift
@Test("Validates email format")                                    // display name
@Test(.tags(.validation, .email))                                  // tags
@Test(.disabled("Server migration in progress"))                   // disabled
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil)) // conditional
@Test(.bug("https://github.com/org/repo/issues/42"))               // bug reference
@Test(.timeLimit(.minutes(1)))                                     // time limit
@Test("Timeout handling", .tags(.networking), .timeLimit(.seconds(30))) // combined
swift
@Test("Validates email format")                                    // 显示名称
@Test(.tags(.validation, .email))                                  // 标签
@Test(.disabled("Server migration in progress"))                   // 禁用
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil)) // 条件启用
@Test(.bug("https://github.com/org/repo/issues/42"))               // 问题引用
@Test(.timeLimit(.minutes(1)))                                     // 时间限制
@Test("Timeout handling", .tags(.networking), .timeLimit(.seconds(30))) // 组合配置

#expect and #require

#expect 和 #require

swift
// #expect records failure but continues execution
#expect(result == 42)
#expect(name.isEmpty == false)
#expect(items.count > 0, "Items should not be empty")

// #expect with error type checking
#expect(throws: ValidationError.self) {
    try validate(email: "not-an-email")
}

// #expect with specific error value
#expect {
    try validate(email: "")
} throws: { error in
    guard let err = error as? ValidationError else { return false }
    return err == .empty
}

// #require records failure AND stops test (like XCTUnwrap)
let user = try #require(await fetchUser(id: 1))
#expect(user.name == "Alice")

// #require for optionals -- unwraps or fails
let first = try #require(items.first)
#expect(first.isValid)
Rule: Use
#require
when subsequent assertions depend on the value. Use
#expect
for independent checks.
swift
// #expect 记录失败但继续执行
#expect(result == 42)
#expect(name.isEmpty == false)
#expect(items.count > 0, "Items should not be empty")

// #expect 错误类型检查
#expect(throws: ValidationError.self) {
    try validate(email: "not-an-email")
}

// #expect 匹配特定错误值
#expect {
    try validate(email: "")
} throws: { error in
    guard let err = error as? ValidationError else { return false }
    return err == .empty
}

// #require 记录失败并终止测试(类似XCTUnwrap)
let user = try #require(await fetchUser(id: 1))
#expect(user.name == "Alice")

// #require 处理可选值 —— 解包失败则测试终止
let first = try #require(items.first)
#expect(first.isValid)
规则:当后续断言依赖当前值时使用
#require
,独立检查使用
#expect

@Suite and Test Organization

@Suite 与测试组织

swift
@Suite("Authentication Tests")
struct AuthTests {
    let auth: AuthService

    // init() runs before EACH test (like setUp in XCTest)
    init() {
        auth = AuthService(store: MockKeychain())
    }

    @Test func loginWithValidCredentials() async throws {
        let result = try await auth.login(email: "test@test.com", password: "pass123")
        #expect(result.isAuthenticated)
    }

    @Test func loginWithInvalidPassword() async throws {
        #expect(throws: AuthError.invalidCredentials) {
            try await auth.login(email: "test@test.com", password: "wrong")
        }
    }
}
Suites can be nested. Tags applied to a suite are inherited by all tests in that suite.
swift
@Suite("Authentication Tests")
struct AuthTests {
    let auth: AuthService

    // init() 在每个测试执行前运行(类似XCTest中的setUp)
    init() {
        auth = AuthService(store: MockKeychain())
    }

    @Test func loginWithValidCredentials() async throws {
        let result = try await auth.login(email: "test@test.com", password: "pass123")
        #expect(result.isAuthenticated)
    }

    @Test func loginWithInvalidPassword() async throws {
        #expect(throws: AuthError.invalidCredentials) {
            try await auth.login(email: "test@test.com", password: "wrong")
        }
    }
}
测试套件可以嵌套。套件上的标签会被所有包含的测试继承。

Parameterized Tests

参数化测试

swift
@Test("Email validation", arguments: [
    ("user@example.com", true),
    ("user@", false),
    ("@example.com", false),
    ("", false),
])
func validateEmail(email: String, isValid: Bool) {
    #expect(EmailValidator.isValid(email) == isValid)
}

// From CaseIterable
@Test(arguments: Currency.allCases)
func currencyHasSymbol(currency: Currency) {
    #expect(currency.symbol.isEmpty == false)
}

// Two collections: cartesian product of all combinations
@Test(arguments: [1, 2, 3], ["a", "b"])
func combinations(number: Int, letter: String) {
    #expect(number > 0)
}

// Use zip for 1:1 pairing instead of cartesian product
@Test(arguments: zip(["USD", "EUR"], ["$", "€"]))
func currencySymbols(code: String, symbol: String) {
    #expect(Currency(code: code).symbol == symbol)
}
Each argument combination runs as an independent test case reported separately.
swift
@Test("Email validation", arguments: [
    ("user@example.com", true),
    ("user@", false),
    ("@example.com", false),
    ("", false),
])
func validateEmail(email: String, isValid: Bool) {
    #expect(EmailValidator.isValid(email) == isValid)
}

// 从CaseIterable生成参数
@Test(arguments: Currency.allCases)
func currencyHasSymbol(currency: Currency) {
    #expect(currency.symbol.isEmpty == false)
}

// 两个集合:所有组合的笛卡尔积
@Test(arguments: [1, 2, 3], ["a", "b"])
func combinations(number: Int, letter: String) {
    #expect(number > 0)
}

// 使用zip进行1:1配对而非笛卡尔积
@Test(arguments: zip(["USD", "EUR"], ["$", "€"]))
func currencySymbols(code: String, symbol: String) {
    #expect(Currency(code: code).symbol == symbol)
}
每个参数组合会作为独立的测试用例运行,并单独报告结果。

Confirmation (Async Event Testing)

Confirmation(异步事件测试)

Replace XCTest's
expectation
/
fulfill
/
waitForExpectations
with
confirmation
:
swift
@Test func notificationPosted() async {
    await confirmation("Received notification") { confirm in
        let observer = NotificationCenter.default.addObserver(
            forName: .userLoggedIn, object: nil, queue: .main
        ) { _ in confirm() }
        await authService.login()
        NotificationCenter.default.removeObserver(observer)
    }
}

// Exact count -- confirm must be called exactly 3 times
await confirmation("Items processed", expectedCount: 3) { confirm in
    processor.onItemComplete = { _ in confirm() }
    await processor.processAll()
}

// Range-based: at least once
await confirmation("Orders placed", expectedCount: 1...) { confirm in
    truck.orderHandler = { _ in confirm() }
    await truck.operate()
}

// Confirm something does NOT happen
await confirmation("No errors", expectedCount: 0) { confirm in
    calculator.errorHandler = { _ in confirm() }
    await calculator.compute()
}
使用
confirmation
替代XCTest中的
expectation
/
fulfill
/
waitForExpectations
swift
@Test func notificationPosted() async {
    await confirmation("Received notification") { confirm in
        let observer = NotificationCenter.default.addObserver(
            forName: .userLoggedIn, object: nil, queue: .main
        ) { _ in confirm() }
        await authService.login()
        NotificationCenter.default.removeObserver(observer)
    }
}

// 精确计数 —— confirm必须被调用恰好3次
await confirmation("Items processed", expectedCount: 3) { confirm in
    processor.onItemComplete = { _ in confirm() }
    await processor.processAll()
}

// 范围限制:至少调用一次
await confirmation("Orders placed", expectedCount: 1...) { confirm in
    truck.orderHandler = { _ in confirm() }
    await truck.operate()
}

// 确认某事件不会发生
await confirmation("No errors", expectedCount: 0) { confirm in
    calculator.errorHandler = { _ in confirm() }
    await calculator.compute()
}

Tags

标签

Define custom tags for filtering and organizing test runs:
swift
extension Tag {
    @Tag static var networking: Self
    @Tag static var database: Self
    @Tag static var slow: Self
    @Tag static var critical: Self
}

@Test(.tags(.networking, .slow))
func downloadLargeFile() async throws { ... }

// Tags on suites are inherited by all contained tests
@Suite(.tags(.database))
struct DatabaseTests {
    @Test func insertUser() { ... }  // inherits .database tag
}
Tags must be declared as static members in an extension on
Tag
using the
@Tag
macro.
定义自定义标签以过滤和组织测试运行:
swift
extension Tag {
    @Tag static var networking: Self
    @Tag static var database: Self
    @Tag static var slow: Self
    @Tag static var critical: Self
}

@Test(.tags(.networking, .slow))
func downloadLargeFile() async throws { ... }

// 套件上的标签会被所有包含的测试继承
@Suite(.tags(.database))
struct DatabaseTests {
    @Test func insertUser() { ... }  // 继承.database标签
}
标签必须在Tag的扩展中使用
@Tag
宏声明为静态成员。

Known Issues

已知问题

Mark expected failures so they do not cause test failure:
swift
withKnownIssue("Propane tank is empty") {
    #expect(truck.grill.isHeating)
}

// Intermittent / flaky failures
withKnownIssue(isIntermittent: true) {
    #expect(service.isReachable)
}

// Conditional known issue
withKnownIssue {
    #expect(foodTruck.grill.isHeating)
} when: {
    !hasPropane
}

// Match specific issues only
try withKnownIssue {
    let level = try #require(foodTruck.batteryLevel)
    #expect(level >= 0.8)
} matching: { issue in
    guard case .expectationFailed(let expectation) = issue.kind else { return false }
    return expectation.isRequired
}
If no known issues are recorded, Swift Testing records a distinct issue notifying you the problem may be resolved.
标记预期失败的测试,避免其导致整体测试失败:
swift
withKnownIssue("Propane tank is empty") {
    #expect(truck.grill.isHeating)
}

// 间歇性/不稳定的失败
withKnownIssue(isIntermittent: true) {
    #expect(service.isReachable)
}

// 条件性已知问题
withKnownIssue {
    #expect(foodTruck.grill.isHeating)
} when: {
    !hasPropane
}

// 仅匹配特定问题
try withKnownIssue {
    let level = try #require(foodTruck.batteryLevel)
    #expect(level >= 0.8)
} matching: { issue in
    guard case .expectationFailed(let expectation) = issue.kind else { return false }
    return expectation.isRequired
}
如果没有记录到已知问题,Swift Testing会记录一个独特的问题,提示你该问题可能已解决。

TestScoping (Custom Test Lifecycle)

TestScoping(自定义测试生命周期)

Consolidate setup/teardown logic (Swift 6.1+, Xcode 16.3+):
swift
struct DatabaseFixture: TestScoping {
    func provideScope(
        for test: Test, testCase: Test.Case?,
        performing body: @Sendable () async throws -> Void
    ) async throws {
        let db = try await TestDatabase.create()
        try await body()
        try await db.destroy()
    }
}
Use
.serialized
on suites where tests share mutable state and cannot run concurrently.
整合初始化/清理逻辑(要求Swift 6.1+、Xcode 16.3+):
swift
struct DatabaseFixture: TestScoping {
    func provideScope(
        for test: Test, testCase: Test.Case?,
        performing body: @Sendable () async throws -> Void
    ) async throws {
        let db = try await TestDatabase.create()
        try await body()
        try await db.destroy()
    }
}
当测试共享可变状态且无法并发运行时,在套件上使用
.serialized

Mocking and Test Doubles

Mocking 与测试替身

Every external dependency should be behind a protocol. Inject dependencies -- never hardcode them:
swift
protocol UserRepository: Sendable {
    func fetch(id: String) async throws -> User
    func save(_ user: User) async throws
}

// Test double
struct MockUserRepository: UserRepository {
    var users: [String: User] = [:]
    var fetchError: Error?
    func fetch(id: String) async throws -> User {
        if let error = fetchError { throw error }
        guard let user = users[id] else { throw NotFoundError() }
        return user
    }
    func save(_ user: User) async throws { }
}
所有外部依赖都应基于协议实现。注入依赖项——绝不硬编码:
swift
protocol UserRepository: Sendable {
    func fetch(id: String) async throws -> User
    func save(_ user: User) async throws
}

// 测试替身
struct MockUserRepository: UserRepository {
    var users: [String: User] = [:]
    var fetchError: Error?
    func fetch(id: String) async throws -> User {
        if let error = fetchError { throw error }
        guard let user = users[id] else { throw NotFoundError() }
        return user
    }
    func save(_ user: User) async throws { }
}

Testable Architecture

可测试架构

swift
@Observable
class ProfileViewModel {
    private let repository: UserRepository
    var user: User?
    var error: Error?
    init(repository: UserRepository) { self.repository = repository }
    func load() async {
        do { user = try await repository.fetch(id: currentUserID) }
        catch { self.error = error }
    }
}

@Test func loadUserSuccess() async {
    let mock = MockUserRepository(users: ["1": User(name: "Alice")])
    let vm = ProfileViewModel(repository: mock)
    await vm.load()
    #expect(vm.user?.name == "Alice")
}
swift
@Observable
class ProfileViewModel {
    private let repository: UserRepository
    var user: User?
    var error: Error?
    init(repository: UserRepository) { self.repository = repository }
    func load() async {
        do { user = try await repository.fetch(id: currentUserID) }
        catch { self.error = error }
    }
}

@Test func loadUserSuccess() async {
    let mock = MockUserRepository(users: ["1": User(name: "Alice")])
    let vm = ProfileViewModel(repository: mock)
    await vm.load()
    #expect(vm.user?.name == "Alice")
}

SwiftUI Environment Injection

SwiftUI 环境注入

swift
private struct UserRepositoryKey: EnvironmentKey {
    static let defaultValue: any UserRepository = RemoteUserRepository()
}
extension EnvironmentValues {
    var userRepository: any UserRepository {
        get { self[UserRepositoryKey.self] }
        set { self[UserRepositoryKey.self] = newValue }
    }
}
// In previews and tests:
ContentView().environment(\.userRepository, MockUserRepository())
swift
private struct UserRepositoryKey: EnvironmentKey {
    static let defaultValue: any UserRepository = RemoteUserRepository()
}
extension EnvironmentValues {
    var userRepository: any UserRepository {
        get { self[UserRepositoryKey.self] }
        set { self[UserRepositoryKey.self] = newValue }
    }
}
// 在预览和测试中使用:
ContentView().environment(\.userRepository, MockUserRepository())

Async and Concurrent Test Patterns

异步与并发测试模式

Swift Testing supports async natively. Use
@MainActor
for tests touching MainActor-isolated code:
swift
@Test func fetchUser() async throws {
    let service = UserService(repository: MockUserRepository())
    let user = try await service.fetch(id: "1")
    #expect(user.name == "Alice")
}

@Test @MainActor func viewModelUpdatesOnMainActor() async {
    let vm = ProfileViewModel(repository: MockUserRepository())
    await vm.load()
    #expect(vm.user != nil)
}
Swift Testing原生支持异步。对于涉及MainActor隔离代码的测试,使用
@MainActor
swift
@Test func fetchUser() async throws {
    let service = UserService(repository: MockUserRepository())
    let user = try await service.fetch(id: "1")
    #expect(user.name == "Alice")
}

@Test @MainActor func viewModelUpdatesOnMainActor() async {
    let vm = ProfileViewModel(repository: MockUserRepository())
    await vm.load()
    #expect(vm.user != nil)
}

Clock Injection

时钟注入

Inject a clock protocol to test time-dependent code without real delays:
swift
protocol AppClock: Sendable { func sleep(for duration: Duration) async throws }
struct ImmediateClock: AppClock { func sleep(for duration: Duration) async throws { } }
注入时钟协议以测试依赖时间的代码,无需真实延迟:
swift
protocol AppClock: Sendable { func sleep(for duration: Duration) async throws }
struct ImmediateClock: AppClock { func sleep(for duration: Duration) async throws { } }

Testing Error Paths

测试错误路径

swift
@Test func fetchUserNetworkError() async {
    var mock = MockUserRepository()
    mock.fetchError = URLError(.notConnectedToInternet)
    let vm = ProfileViewModel(repository: mock)
    await vm.load()
    #expect(vm.user == nil)
    #expect(vm.error is URLError)
}
swift
@Test func fetchUserNetworkError() async {
    var mock = MockUserRepository()
    mock.fetchError = URLError(.notConnectedToInternet)
    let vm = ProfileViewModel(repository: mock)
    await vm.load()
    #expect(vm.user == nil)
    #expect(vm.error is URLError)
}

XCTest: UI Testing

XCTest:UI测试

Swift Testing does not support UI testing. Use XCTest with XCUITest for all UI tests.
swift
class LoginUITests: XCTestCase {
    let app = XCUIApplication()
    override func setUpWithError() throws {
        continueAfterFailure = false
        app.launchArguments = ["--ui-testing"]
        app.launch()
    }
    func testLoginFlow() throws {
        let emailField = app.textFields["Email"]
        XCTAssertTrue(emailField.waitForExistence(timeout: 5))
        emailField.tap(); emailField.typeText("user@test.com")
        app.secureTextFields["Password"].tap()
        app.secureTextFields["Password"].typeText("password123")
        app.buttons["Sign In"].tap()
        XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 10))
    }
}
Swift Testing不支持UI测试,所有UI测试使用XCTest搭配XCUITest。
swift
class LoginUITests: XCTestCase {
    let app = XCUIApplication()
    override func setUpWithError() throws {
        continueAfterFailure = false
        app.launchArguments = ["--ui-testing"]
        app.launch()
    }
    func testLoginFlow() throws {
        let emailField = app.textFields["Email"]
        XCTAssertTrue(emailField.waitForExistence(timeout: 5))
        emailField.tap(); emailField.typeText("user@test.com")
        app.secureTextFields["Password"].tap()
        app.secureTextFields["Password"].typeText("password123")
        app.buttons["Sign In"].tap()
        XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 10))
    }
}

Page Object Pattern

页面对象模式

Encapsulate UI element queries in page objects for reusable, readable UI tests:
swift
struct LoginPage {
    let app: XCUIApplication
    var emailField: XCUIElement { app.textFields["Email"] }
    var passwordField: XCUIElement { app.secureTextFields["Password"] }
    var signInButton: XCUIElement { app.buttons["Sign In"] }
    @discardableResult
    func login(email: String, password: String) -> HomePage {
        emailField.tap(); emailField.typeText(email)
        passwordField.tap(); passwordField.typeText(password)
        signInButton.tap()
        return HomePage(app: app)
    }
}
将UI元素查询封装到页面对象中,实现可复用、可读性强的UI测试:
swift
struct LoginPage {
    let app: XCUIApplication
    var emailField: XCUIElement { app.textFields["Email"] }
    var passwordField: XCUIElement { app.secureTextFields["Password"] }
    var signInButton: XCUIElement { app.buttons["Sign In"] }
    @discardableResult
    func login(email: String, password: String) -> HomePage {
        emailField.tap(); emailField.typeText(email)
        passwordField.tap(); passwordField.typeText(password)
        signInButton.tap()
        return HomePage(app: app)
    }
}

Performance Testing (XCTest)

性能测试(XCTest)

swift
func testFeedParsingPerformance() throws {
    let data = try loadFixture("large-feed.json")
    let metrics: [XCTMetric] = [XCTClockMetric(), XCTMemoryMetric()]
    measure(metrics: metrics) {
        _ = try? FeedParser.parse(data)
    }
}
swift
func testFeedParsingPerformance() throws {
    let data = try loadFixture("large-feed.json")
    let metrics: [XCTMetric] = [XCTClockMetric(), XCTMemoryMetric()]
    measure(metrics: metrics) {
        _ = try? FeedParser.parse(data)
    }
}

Snapshot Testing

快照测试

Use swift-snapshot-testing (pointfreeco) for visual regression. Requires XCTest:
swift
import SnapshotTesting
import XCTest

class ProfileViewSnapshotTests: XCTestCase {
    func testProfileView() {
        let view = ProfileView(user: .preview)
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))

        // Dark mode
        assertSnapshot(of: view.environment(\.colorScheme, .dark),
                       as: .image(layout: .device(config: .iPhone13)), named: "dark")

        // Large Dynamic Type
        assertSnapshot(of: view.environment(\.dynamicTypeSize, .accessibility3),
                       as: .image(layout: .device(config: .iPhone13)), named: "largeText")
    }
}
Always test Dark Mode and large Dynamic Type in snapshots.
使用swift-snapshot-testing(pointfreeco)进行视觉回归测试,需要依赖XCTest:
swift
import SnapshotTesting
import XCTest

class ProfileViewSnapshotTests: XCTestCase {
    func testProfileView() {
        let view = ProfileView(user: .preview)
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))

        // 深色模式
        assertSnapshot(of: view.environment(\.colorScheme, .dark),
                       as: .image(layout: .device(config: .iPhone13)), named: "dark")

        // 大动态字体
        assertSnapshot(of: view.environment(\.dynamicTypeSize, .accessibility3),
                       as: .image(layout: .device(config: .iPhone13)), named: "largeText")
    }
}
快照测试中务必测试深色模式和大动态字体。

Test File Organization

测试文件组织

Tests/AppTests/          # Swift Testing (Models/, ViewModels/, Services/)
Tests/AppUITests/        # XCTest UI tests (Pages/, Flows/)
Tests/Fixtures/          # Test data (JSON, images)
Tests/Mocks/             # Shared mock implementations
Name test files
<TypeUnderTest>Tests.swift
. Describe behavior in function names:
fetchUserReturnsNilOnNetworkError()
not
testFetchUser()
. Name mocks
Mock<ProtocolName>
.
Tests/AppTests/          # Swift Testing测试(Models/、ViewModels/、Services/)
Tests/AppUITests/        # XCTest UI测试(Pages/、Flows/)
Tests/Fixtures/          # 测试数据(JSON、图片)
Tests/Mocks/             # 共享Mock实现
测试文件命名为
<TypeUnderTest>Tests.swift
。函数名需描述行为:例如
fetchUserReturnsNilOnNetworkError()
而非
testFetchUser()
。Mock命名为
Mock<ProtocolName>

What to Test

测试范围

Always test: business logic, validation rules, state transitions in view models, error handling paths, edge cases (empty collections, nil, boundaries), async success and failure, Task cancellation.
Skip: SwiftUI view body layout (use snapshots), simple property forwarding, Apple framework behavior, private methods (test through public API).
务必测试: 业务逻辑、验证规则、视图模型中的状态转换、错误处理路径、边缘情况(空集合、nil、边界值)、异步成功与失败场景、Task取消。
无需测试: SwiftUI视图布局(使用快照测试)、简单属性转发、Apple框架行为、私有方法(通过公共API测试)。

Common Mistakes

常见错误

  1. Testing implementation, not behavior. Test what the code does, not how.
  2. No error path tests. If a function can throw, test the throw path.
  3. Flaky async tests. Use
    confirmation
    with expected counts, not
    sleep
    calls.
  4. Shared mutable state between tests. Each test sets up its own state via
    init()
    in
    @Suite
    .
  5. Missing accessibility identifiers in UI tests. XCUITest queries rely on them.
  6. Using
    sleep
    in tests.
    Use
    confirmation
    , clock injection, or
    withKnownIssue
    .
  7. Not testing cancellation. If code supports
    Task
    cancellation, verify it cancels cleanly.
  8. Mixing XCTest and Swift Testing in one file. Keep them in separate files.
  9. Non-Sendable test helpers shared across tests. Ensure test helper types are Sendable when shared across concurrent test cases. Annotate MainActor-dependent test code with
    @MainActor
    .
  1. 测试实现细节而非行为。 测试代码的功能,而非实现方式。
  2. 未测试错误路径。 如果函数可能抛出错误,务必测试抛出场景。
  3. 不稳定的异步测试。 使用带预期计数的
    confirmation
    ,而非
    sleep
    调用。
  4. 测试间共享可变状态。 每个测试通过
    @Suite
    中的
    init()
    设置自身状态。
  5. UI测试中缺少可访问性标识符。 XCUITest查询依赖这些标识符。
  6. 在测试中使用
    sleep
    使用
    confirmation
    、时钟注入或
    withKnownIssue
  7. 未测试取消逻辑。 如果代码支持
    Task
    取消,需验证其能否干净地取消。
  8. 在一个文件中混合XCTest和Swift Testing。 将它们放在不同文件中。
  9. 跨测试共享非Sendable的测试助手。 当跨并发测试用例共享测试助手类型时,确保其为Sendable。为依赖MainActor的测试代码添加
    @MainActor
    注解。

Test Attachments

测试附件

Attach diagnostic data to test results for debugging failures:
swift
@Test func generateReport() async throws {
    let report = try generateReport()
    // Attach the output for later inspection
    Attachment(report.data, named: "report.json").record()
    #expect(report.isValid)
}

// Attach from a file URL
@Test func processImage() async throws {
    let output = try processImage()
    try await Attachment(contentsOf: output.url, named: "result.png")
        .record()
}
Attachments support any
Attachable
type and images via
AttachableAsImage
.
将诊断数据附加到测试结果中,便于调试失败用例:
swift
@Test func generateReport() async throws {
    let report = try generateReport()
    // 附加输出供后续检查
    Attachment(report.data, named: "report.json").record()
    #expect(report.isValid)
}

// 从文件URL附加
@Test func processImage() async throws {
    let output = try processImage()
    try await Attachment(contentsOf: output.url, named: "result.png")
        .record()
}
附件支持任何
Attachable
类型,以及通过
AttachableAsImage
添加图片。

Exit Testing

退出测试

Test code that calls
exit()
,
fatalError()
, or
preconditionFailure()
:
swift
@Test func invalidInputCausesExit() async {
    await #expect(processExitsWith: .failure) {
        processInvalidInput()  // calls fatalError()
    }
}
测试调用
exit()
fatalError()
preconditionFailure()
的代码:
swift
@Test func invalidInputCausesExit() async {
    await #expect(processExitsWith: .failure) {
        processInvalidInput()  // 调用fatalError()
    }
}

Review Checklist

审查清单

  • All new tests use Swift Testing (
    @Test
    ,
    #expect
    ), not XCTest assertions
  • Test names describe behavior (
    fetchUserReturnsNilOnNetworkError
    not
    testFetchUser
    )
  • Error paths have dedicated tests
  • Async tests use
    confirmation()
    , not
    Task.sleep
  • Parameterized tests used for repetitive variations
  • Tags applied for filtering (
    .critical
    ,
    .slow
    )
  • Mocks conform to protocols, not subclass concrete types
  • No shared mutable state between tests
  • Cancellation tested for cancellable async operations
  • 所有新测试使用Swift Testing(
    @Test
    #expect
    ),而非XCTest断言
  • 测试名称描述行为(
    fetchUserReturnsNilOnNetworkError
    而非
    testFetchUser
  • 错误路径有专门的测试
  • 异步测试使用
    confirmation()
    ,而非
    Task.sleep
  • 使用参数化测试处理重复的变体场景
  • 应用标签用于过滤(
    .critical
    .slow
  • Mock遵循协议,而非子类化具体类型
  • 测试间无共享可变状态
  • 对可取消的异步操作测试取消逻辑

MCP Integration

MCP 集成

  • xcodebuildmcp: Build and run tests directly — full suites, individual functions, tag-filtered runs.
  • xcodebuildmcp:直接构建并运行测试 —— 支持完整套件、单个函数、按标签过滤的测试运行。