swift-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwift 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))) // combinedswift
@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 when subsequent assertions depend on the value. Use for independent checks.
#require#expectswift
// #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 // with :
expectationfulfillwaitForExpectationsconfirmationswift
@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()
}使用替代XCTest中的//:
confirmationexpectationfulfillwaitForExpectationsswift
@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 using the macro.
Tag@Tag定义自定义标签以过滤和组织测试运行:
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的扩展中使用宏声明为静态成员。
@TagKnown 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 on suites where tests share mutable state and cannot run concurrently.
.serialized整合初始化/清理逻辑(要求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()
}
}当测试共享可变状态且无法并发运行时,在套件上使用。
.serializedMocking 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 for tests touching MainActor-isolated code:
@MainActorswift
@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隔离代码的测试,使用:
@MainActorswift
@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 implementationsName test files . Describe behavior in function names: not . Name mocks .
<TypeUnderTest>Tests.swiftfetchUserReturnsNilOnNetworkError()testFetchUser()Mock<ProtocolName>Tests/AppTests/ # Swift Testing测试(Models/、ViewModels/、Services/)
Tests/AppUITests/ # XCTest UI测试(Pages/、Flows/)
Tests/Fixtures/ # 测试数据(JSON、图片)
Tests/Mocks/ # 共享Mock实现测试文件命名为。函数名需描述行为:例如而非。Mock命名为。
<TypeUnderTest>Tests.swiftfetchUserReturnsNilOnNetworkError()testFetchUser()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
常见错误
- Testing implementation, not behavior. Test what the code does, not how.
- No error path tests. If a function can throw, test the throw path.
- Flaky async tests. Use with expected counts, not
confirmationcalls.sleep - Shared mutable state between tests. Each test sets up its own state via in
init().@Suite - Missing accessibility identifiers in UI tests. XCUITest queries rely on them.
- Using in tests. Use
sleep, clock injection, orconfirmation.withKnownIssue - Not testing cancellation. If code supports cancellation, verify it cancels cleanly.
Task - Mixing XCTest and Swift Testing in one file. Keep them in separate files.
- 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
- 测试实现细节而非行为。 测试代码的功能,而非实现方式。
- 未测试错误路径。 如果函数可能抛出错误,务必测试抛出场景。
- 不稳定的异步测试。 使用带预期计数的,而非
confirmation调用。sleep - 测试间共享可变状态。 每个测试通过中的
@Suite设置自身状态。init() - UI测试中缺少可访问性标识符。 XCUITest查询依赖这些标识符。
- 在测试中使用。 使用
sleep、时钟注入或confirmation。withKnownIssue - 未测试取消逻辑。 如果代码支持取消,需验证其能否干净地取消。
Task - 在一个文件中混合XCTest和Swift Testing。 将它们放在不同文件中。
- 跨测试共享非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 type and images via .
AttachableAttachableAsImage将诊断数据附加到测试结果中,便于调试失败用例:
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()
}附件支持任何类型,以及通过添加图片。
AttachableAttachableAsImageExit Testing
退出测试
Test code that calls , , or :
exit()fatalError()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), not XCTest assertions#expect - Test names describe behavior (not
fetchUserReturnsNilOnNetworkError)testFetchUser - Error paths have dedicated tests
- Async tests use , not
confirmation()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),而非XCTest断言#expect - 测试名称描述行为(而非
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:直接构建并运行测试 —— 支持完整套件、单个函数、按标签过滤的测试运行。