swift-tdd
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOverview
概述
The fundamental approach is: Write the test first. Watch it fail. Write minimal code to pass.
Core principle: "If you didn't watch the test fail, you don't know if it tests the right thing."
This skill combines Test-Driven Development discipline with Swift Testing, SwiftUI, and Swift Concurrency best practices.
核心方法是:先编写测试,观察测试失败,再编写最少的代码让测试通过。
核心原则:“如果你没有看到测试失败,你就不知道测试是否针对了正确的内容。”
本技能将测试驱动开发(TDD)规范与Swift Testing、SwiftUI和Swift并发的最佳实践相结合。
When to Use
使用场景
Apply TDD consistently for:
- New features (SwiftUI views, view models, services)
- Bug fixes
- Refactoring
- Behavior changes
- Async/concurrent logic
Exceptions require explicit human approval and typically only apply to throwaway prototypes, generated code, asset catalogs, or configuration plists.
在以下场景中持续应用TDD:
- 新功能(SwiftUI视图、视图模型、服务)
- Bug修复
- 代码重构
- 行为变更
- 异步/并发逻辑
例外情况需要明确的人工批准,通常仅适用于一次性原型、生成的代码、资源目录或配置plist文件。
The Iron Law
铁律
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST.
Code written before tests must be deleted completely and reimplemented through TDD, without keeping it as reference material.
没有先编写失败的测试,就不能编写生产代码。
在测试之前编写的代码必须完全删除,并通过TDD重新实现,不能将其作为参考资料保留。
Red-Green-Refactor Cycle
红-绿-重构循环
RED: Write one minimal failing test demonstrating required behavior
GREEN: Implement the simplest Swift code that passes the test
REFACTOR: Clean up code while keeping all tests green
**红:**编写一个最小化的失败测试,展示所需的行为
**绿:**编写最简单的Swift代码让测试通过
**重构:**在保持所有测试通过的同时清理代码
Testing Rules by Layer
按层级划分的测试规则
View Models (@Observable
classes)
@Observable视图模型(@Observable
类)
@Observable- Test logic and state changes; do not test SwiftUI rendering.
- Mark view models and call them from
@MainActortest contexts.@MainActor - Inject dependencies (network, persistence) via protocols so tests can substitute fakes.
swift
@MainActor
@Suite("ProfileViewModel")
struct ProfileViewModelTests {
@Test("loading sets user name")
func loadingSetsUserName() async throws {
let fake = FakeUserService(stub: User(name: "Ada"))
let vm = ProfileViewModel(service: fake)
await vm.load()
#expect(vm.userName == "Ada")
}
}- 测试逻辑和状态变更;不要测试SwiftUI渲染。
- 将视图模型标记为,并在
@MainActor测试上下文中调用它们。@MainActor - 通过协议注入依赖项(网络、持久化),以便测试可以替换为假实现(Fake)。
swift
@MainActor
@Suite("ProfileViewModel")
struct ProfileViewModelTests {
@Test("loading sets user name")
func loadingSetsUserName() async throws {
let fake = FakeUserService(stub: User(name: "Ada"))
let vm = ProfileViewModel(service: fake)
await vm.load()
#expect(vm.userName == "Ada")
}
}Services and Repositories
服务与仓库
- Use in-memory or fake implementations, not real network/database.
- Use to unwrap optionals when subsequent assertions depend on them.
#require
- 使用内存中的假实现(Fake)或存根(Stub),而非真实的网络/数据库。
- 当后续断言依赖于可选值时,使用来解包可选值。
#require
Async / Concurrent Code
异步/并发代码
- Mark test functions and
asyncall async calls — never spin or sleep.await - Bridge callback-based APIs with or
withCheckedContinuationrather thanAsyncStream.XCTestExpectation - Use for callback-based event verification.
confirmation()
- 将测试函数标记为,并
async所有异步调用——绝不要使用自旋或睡眠。await - 使用或
withCheckedContinuation桥接基于回调的API,而非AsyncStream。XCTestExpectation - 使用验证基于回调的事件。
confirmation()
SwiftUI Views
SwiftUI视图
SwiftUI should not be unit-tested directly. Instead:
body- Extract logic into view models and test the model.
@Observable - Extract pure helpers (formatters, validators) as free functions or types and test those.
- Use UI tests () only for end-to-end critical paths.
XCUIApplication
swift
// GOOD — test the model that drives the view
@Test("submit disables when title is empty")
func submitDisabledWhenTitleEmpty() {
let vm = NewPostViewModel()
vm.title = ""
#expect(vm.isSubmitDisabled == true)
}
// AVOID — testing SwiftUI rendering or view hierarchy directly in unit tests不应直接对SwiftUI的进行单元测试。而是:
body- 提取逻辑到视图模型中,测试该模型。
@Observable - 提取纯辅助函数(格式化器、验证器)作为自由函数或类型,并测试这些内容。
- 仅对端到端关键路径使用UI测试()。
XCUIApplication
swift
// 推荐做法 — 测试驱动视图的模型
@Test("submit disables when title is empty")
func submitDisabledWhenTitleEmpty() {
let vm = NewPostViewModel()
vm.title = ""
#expect(vm.isSubmitDisabled == true)
}
// 避免做法 — 在单元测试中直接测试SwiftUI渲染或视图层级Fake/Stub Strategy
Fake/Stub策略
Use real code unless a boundary forces isolation:
| Boundary | Strategy |
|---|---|
| Network | Protocol + in-memory fake |
| Database | In-memory repository |
| System clock | Injected |
| File system | Temp directory per test |
| Fake conforming to same protocol |
Never mock view models themselves — test them directly.
@Observable除非边界强制隔离,否则使用真实代码:
| 边界 | 策略 |
|---|---|
| 网络 | 协议 + 内存中的Fake |
| 数据库 | 内存中的仓库 |
| 系统时钟 | 注入 |
| 文件系统 | 每个测试使用临时目录 |
| 符合相同协议的Fake |
绝不要Mock 视图模型本身——直接测试它们。
@ObservableRed Flags for Incomplete TDD
不完整TDD的危险信号
- Code was written before the test
- Test passed immediately on first run (test may not exercise the right thing)
- Cannot explain what the failure message means
- "I'll add tests after" or "it's too simple to test"
- Test only verifies mock calls, not actual behavior
- 代码在测试之前就已编写
- 测试首次运行就立即通过(测试可能没有覆盖正确的内容)
- 无法解释失败消息的含义
- "我之后再添加测试"或"它太简单了不需要测试"
- 测试仅验证Mock调用,而非实际行为
Common Rationalizations — Addressed
常见借口的回应
- "SwiftUI views are hard to test" — Test the view model, not the view.
- "async code is tricky" — Write the async test first; let the compiler guide the implementation.
- "Too simple to test" — Simple logic is easiest to TDD. Start there.
- "I'll test after" — Code written before tests is tested after. That violates the iron law.
- "Deleting code is wasteful" — Sunk-cost fallacy. Delete it and re-implement with TDD.
- "SwiftUI视图很难测试" — 测试视图模型,而非视图本身。
- "异步代码很棘手" — 先编写异步测试;让编译器引导实现。
- "太简单了不需要测试" — 简单逻辑最适合用TDD实现。从这里开始。
- "我之后再测试" — 在测试之前编写的代码属于事后测试,这违反了铁律。
- "删除代码是浪费" — 这是沉没成本谬误。删除代码并通过TDD重新实现。
Verification Checklist
验证清单
Before marking any task complete:
- Every new function, method, or computed property has at least one test
- Each test was watched failing before implementation was written
- Minimal production code was written — no speculative logic
- All tests pass with clean output (or Xcode test navigator green)
swift test - Async tests use , not sleeps or expectations
await - Parameterized tests replace repetitive test methods
- No test-only methods leaked into production types
- View models are tested; SwiftUI is not
body - Fakes, not mocks, used at real system boundaries
在标记任务完成之前:
- 每个新函数、方法或计算属性至少有一个测试
- 每个测试在编写实现代码之前都被观察到失败
- 编写了最少的生产代码——没有投机性逻辑
- 所有测试都通过且输出干净(或Xcode测试导航器显示绿色)
swift test - 异步测试使用,而非睡眠或预期(Expectation)
await - 使用参数化测试替代重复的测试方法
- 没有仅用于测试的方法泄露到生产类型中
- 测试了视图模型;未测试SwiftUI的
body - 在真实系统边界使用Fake而非Mock
Anti-Patterns Reference
反模式参考
See for Swift-specific testing anti-patterns and how to fix them.
testing-anti-patterns.md请查看了解Swift特定的测试反模式及修复方法。
testing-anti-patterns.mdRelated Skills
相关技能
This skill works in conjunction with:
- swift-testing-expert — Deep reference on Swift Testing APIs, traits, async waiting, XCTest migration
- swiftui-expert-skill — SwiftUI state management, view composition, modern APIs
- swift-concurrency — Actors, Sendable, task isolation, Swift 6 migration
本技能与以下技能配合使用:
- swift-testing-expert — Swift Testing API、特性、异步等待、XCTest迁移的深度参考
- swiftui-expert-skill — SwiftUI状态管理、视图组合、现代API
- swift-concurrency — 参与者(Actor)、Sendable、任务隔离、Swift 6迁移