swift-tdd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Overview

概述

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
类)

  • Test logic and state changes; do not test SwiftUI rendering.
  • Mark view models
    @MainActor
    and call them from
    @MainActor
    test contexts.
  • 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
    #require
    to unwrap optionals when subsequent assertions depend on them.
  • 使用内存中的假实现(Fake)或存根(Stub),而非真实的网络/数据库。
  • 当后续断言依赖于可选值时,使用
    #require
    来解包可选值。

Async / Concurrent Code

异步/并发代码

  • Mark test functions
    async
    and
    await
    all async calls — never spin or sleep.
  • Bridge callback-based APIs with
    withCheckedContinuation
    or
    AsyncStream
    rather than
    XCTestExpectation
    .
  • Use
    confirmation()
    for callback-based event verification.
  • 将测试函数标记为
    async
    ,并
    await
    所有异步调用——绝不要使用自旋或睡眠。
  • 使用
    withCheckedContinuation
    AsyncStream
    桥接基于回调的API,而非
    XCTestExpectation
  • 使用
    confirmation()
    验证基于回调的事件。

SwiftUI Views

SwiftUI视图

SwiftUI
body
should not be unit-tested directly. Instead:
  1. Extract logic into
    @Observable
    view models and test the model.
  2. Extract pure helpers (formatters, validators) as free functions or types and test those.
  3. Use UI tests (
    XCUIApplication
    ) only for end-to-end critical paths.
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
进行单元测试。而是:
  1. 提取逻辑
    @Observable
    视图模型中,测试该模型。
  2. 提取纯辅助函数(格式化器、验证器)作为自由函数或类型,并测试这些内容。
  3. 仅对端到端关键路径使用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:
BoundaryStrategy
NetworkProtocol + in-memory fake
DatabaseIn-memory repository
System clockInjected
Clock
/
TimeProvider
File systemTemp directory per test
@Observable
dependencies
Fake conforming to same protocol
Never mock
@Observable
view models themselves — test them directly.
除非边界强制隔离,否则使用真实代码:
边界策略
网络协议 + 内存中的Fake
数据库内存中的仓库
系统时钟注入
Clock
/
TimeProvider
文件系统每个测试使用临时目录
@Observable
依赖项
符合相同协议的Fake
绝不要Mock
@Observable
视图模型本身——直接测试它们。

Red 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 (
    swift test
    or Xcode test navigator green)
  • Async tests use
    await
    , not sleeps or expectations
  • Parameterized tests replace repetitive test methods
  • No test-only methods leaked into production types
  • View models are tested; SwiftUI
    body
    is not
  • Fakes, not mocks, used at real system boundaries
在标记任务完成之前:
  • 每个新函数、方法或计算属性至少有一个测试
  • 每个测试在编写实现代码之前都被观察到失败
  • 编写了最少的生产代码——没有投机性逻辑
  • 所有测试都通过且输出干净(
    swift test
    或Xcode测试导航器显示绿色)
  • 异步测试使用
    await
    ,而非睡眠或预期(Expectation)
  • 使用参数化测试替代重复的测试方法
  • 没有仅用于测试的方法泄露到生产类型中
  • 测试了视图模型;未测试SwiftUI的
    body
  • 在真实系统边界使用Fake而非Mock

Anti-Patterns Reference

反模式参考

See
testing-anti-patterns.md
for Swift-specific testing anti-patterns and how to fix them.
请查看
testing-anti-patterns.md
了解Swift特定的测试反模式及修复方法。

Related 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迁移