TDD 101:测试驱动开发的工程化实践

什么是 TDD?

Test-Driven Development(测试驱动开发)

TDD 是一种 设计方法论(Design Technique),而不是测试技术。说人话就是:

先写测试,再写代码,最后优化。通过快速反馈循环提升代码质量。

核心理念

很多人以为 TDD 就是写更多测试,其实不是。TDD 的核心是用测试来描述你想要的行为,然后让代码去满足这个行为。

就像盖房子前先画图纸一样,TDD 让你在写代码前先想清楚"这玩意儿到底要干什么"。

  • 通过测试用例描述系统行为(Specification by Example)
  • 渐进式设计(Incremental Design)
  • 快速反馈循环(Fast Feedback Loop)
  • 为重构提供安全网,为演进提供信心

TDD 循环:红绿灯法则

TDD 的核心循环就像红绿灯一样简单:

Red(红灯)  :写一个失败的测试(先定义行为)
Green(绿灯):写最少代码让测试通过(让行为成立)
Refactor     :在测试保护下优化结构(让代码更优雅)

常见误解(辟谣时间)

❌ TDD = 写更多测试
→ 错!TDD 的目标不是测试覆盖率,而是更好的设计

❌ TDD 的目标是提高覆盖率
→ 覆盖率只是副产品,不是目标

❌ 写完代码再补测试也算 TDD
→ 不算!TDD 必须是先测试后代码

❌ TDD 会让开发变慢
→ 短期看可能慢一点,长期看反而更快(因为返工少了)

✅ TDD 的真正目标是:更好的设计 + 更快的反馈


为什么我们需要 TDD?

减少需求模糊,明确行为

你有没有遇到过这种情况?

  • 产品说:"这个功能很简单,就是..."
  • 开发写完后,产品说:"不对,我想要的不是这样"
  • 然后开始改,改完又不对,再改...

在大型工程中,这种"理解不一致"简直是家常便饭:

  • 多方对需求理解不一致
  • 文档与实现不一致(文档?什么文档?)
  • 边界情况被遗漏("哦,这个情况我没考虑到...")

TDD 通过"可执行需求"让行为清晰、精确、可验证。测试就是活的需求文档,不会说谎。


快速反馈循环,减少返工

传统流程就像这样:

写代码 → 测试 → 发现不对 → 修 → 再测 → 又不对 → 再修...

反馈周期长,等你发现问题,可能已经写了一大堆代码,改起来成本很高。

TDD 就不一样了:

Red → Green → Refactor

在最短时间内发现行为偏差,把返工成本拉到最低。就像开车时,方向盘稍微偏一点就能立刻感觉到,而不是等到撞墙才发现。


更好的设计质量

你有没有写过那种"测试起来特别困难"的代码?

  • 耦合度高(改一个地方,十个地方都要改)
  • 隐藏状态多(不知道什么时候状态就变了)
  • 单个方法过大(一个方法几百行,不知道在干什么)

可测试性差的代码往往就是设计差的代码。

TDD 强迫你打造:

  • 小函数(一个函数只做一件事)
  • 明确边界(依赖关系清晰)
  • 可替换依赖(方便测试,也方便扩展)

设计自然会变得更清晰、更模块化。这不是魔法,而是被逼出来的。


自信的重构

没有测试,就没有真正的重构。

你只能"改代码",但不敢保证改完后功能还正常。有了 TDD:

  • 重构行为不变的保障(测试告诉你行为没变)
  • 新需求不会破坏旧逻辑(测试会报警)
  • 系统长期保持健康状态(代码不会慢慢腐化)

就像有了安全带,你才敢在高速公路上开车。


提升稳定性

测试先行可以更早发现:

  • 行为理解差异("我以为是这样,结果不是")
  • 各种边界与异常("负数怎么办?空值怎么办?")
  • 多端行为分歧("iOS 和 Android 行为不一样?")
  • 轻度或潜在 bug("这个边界情况会出问题")

越早暴露,修复越便宜。就像体检,早发现早治疗。


提升团队沟通效率

可读的测试用例 = 团队通用语言

让产品、QA、开发对行为理解一致。不需要写文档,测试就是最好的文档。

因此,TDD 可以为我们带来:

  • 更清晰的行为(测试说了算)
  • 更好的设计(被逼出来的)
  • 更快的反馈(Red → Green 很快)
  • 更安全的演进(有测试保护)

我眼中的 TDD

在我看来,TDD 的本质并不是"测试",而是一种将业务行为安全、精准、结构化地落地为代码的方式

它解决的核心矛盾,是 现实世界的模糊业务逻辑软件世界的精确执行机制 之间的巨大鸿沟。

TDD 解决的核心问题:行为的转译

现实世界中的业务规则通常很"人性化":

  • 模糊、语言化、含糊空间大("大概是这样")
  • 强依赖上下文("看情况")
  • 不同角色对同一个需求理解不同("我以为...")
  • 口头描述容易偏差("传话游戏")
  • 边界由经验隐含而非显式表达("这个应该不用考虑吧")

但软件世界要求:

  • 明确(不能"大概")
  • 精确(不能"看情况")
  • 可执行(不能"我以为")
  • 不允许模糊空间(必须说清楚)

因此,业务 → 代码行为 的转化,是现代工程中的关键难题。

TDD 提供的价值就在于:

在编写实现之前,通过测试把行为完整地、可执行地定义清楚。

TDD 的价值:作为"桥梁"的测试

TDD 不是测试工具,它是一个 行为转译器(Behavior Translator)

它用结构化、可执行的方式,将业务逻辑逐条映射到代码层面。

测试在这里的作用不是验证,而是"定义":

  1. 每个测试用例代表一段业务行为

    测试成为团队在行为层面的最小对齐单元。就像乐高积木,每个测试是一个小积木,组合起来就是完整的功能。

  2. 行为被逐条转化为可执行描述

    从口头 → 文本 → 测试 → 代码

    整个转化链条变得清晰可控。每一步都有迹可循。

  3. 偏差在写代码前就能被发现,而不是上线后

    • 需求理解错误("哦,原来是这样")
    • 边界遗漏("负数的情况我没想到")
    • 异常逻辑缺失("网络失败怎么办?")
      都会在 Red 阶段暴露。早发现,早治疗。
  4. 形成统一语言(Ubiquitous Language)

    可读的测试描述让 QA、产品、开发在"行为"层面对齐。大家说同一种语言。

为什么我坚持 TDD:因为它改变了工程方式

TDD 对我来说,是一种确保工程质量的稳定机制:

  • 行为不会丢失

    每个测试是一个需求的锚点。需求不会"说没就没"。

  • 设计不会腐化

    Green 后立刻重构,代码永远在进化。不会慢慢变烂。

  • 重构变得安全而轻松

    因为测试保护了行为边界。有了安全网,才敢做高难度动作。

  • 团队沟通更精确

    通过测试描述行为,而不是靠口头解释。测试不会说谎。

  • 多端或多语言实现变得一致

    测试提供了跨端的"行为契约"。iOS 和 Android 必须行为一致。

TDD 带来的不是写更多测试,而是:

更低的返工成本、更清晰的行为、更健康的设计。

一句话概括我眼中的 TDD

TDD 是一种让行为先落地、让设计持续进化、让代码更安全可控的工程化工作方式。

测试不是目标,而是描述行为、驱动设计的载体。

当你理解了 TDD 的这个本质,所有技巧和工具都会自然融入你的工程体系中。


TDD 如何使用?

TDD 开发循环(核心实践)

1. Red —— 写失败测试

  • 定义行为("我要什么")
  • 明确期望("应该返回什么")
  • 提供设计入口("需要什么接口")

2. Green —— 最小实现

  • 不追求最佳设计(先让它跑起来)
  • 不做重构、不做优化(别想太多)
  • 只为实现行为(能通过测试就行)

3. Refactor —— 在测试保护下优化设计

  • 改进结构、命名、抽象(让代码更优雅)
  • 不改变任何行为(测试必须还通过)

示例:加法器 Calculator 的完整 Red → Green → Refactor 流程

下面用一个最简单的例子,展示 TDD 的三段循环如何工作。

业务需求(Business Behavior):

创建一个 Calculator,能够接收两个整数并返回它们的和。


1. Red —— 写一个失败的测试(定义行为)

首先定义行为,而不是先写实现。

行为描述非常简单:"1 + 2 = 3"。

import XCTest

final class CalculatorTests: XCTestCase {

    func test_add_two_numbers() {
        let c = Calculator()
        let result = c.add(1, 2)
        XCTAssertEqual(result, 3)
    }
}

此时 Calculator 类根本不存在,编译会失败(Red)。

这是好的 —— 因为我们正在描述行为,而不是写实现。就像先画图纸,再盖房子。


2. Green —— 写最少实现让测试通过

此阶段的目标不是写优雅代码,而是让行为成立。

只需要写"最低限度"的代码:

class Calculator {
    func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
}

测试通过(Green)。

注意:

  • 不优化命名(add 这个名字够用了)
  • 不思考扩展性(先不考虑减法、乘法)
  • 不添加额外功能(只做能让测试通过的最小代码)

3. Refactor —— 在测试保护下优化设计

行为已经被测试锁定,现在可以安全优化。

假设我们现在认为:

  • 加法功能未来可能扩展为一系列运算
  • Calculator 应该支持更多操作
  • add 方法应该抽象为 Operation 层,便于扩展

我们进行结构优化:

protocol Operation {
    func apply(_ a: Int, _ b: Int) -> Int
}

struct AddOperation: Operation {
    func apply(_ a: Int, _ b: Int) -> Int {
        a + b
    }
}

class Calculator {
    private let addOperation: Operation

    init(addOperation: Operation = AddOperation()) {
        self.addOperation = addOperation
    }

    func add(_ a: Int, _ b: Int) -> Int {
        addOperation.apply(a, b)
    }
}

测试仍然通过(Refactor 成功)。

行为未变,设计更可扩展、可替换、可测试。这就是 TDD 的魔力。


对比:Red → Green → Refactor 的价值

阶段 关注点 行为是否固定 代码是否优雅
Red 定义行为 ❌ 还未固定(测试失败) 无代码
Green 实现行为 ✅ 固定(测试通过) ❌ 可能很丑
Refactor 优化结构 ✅ 固定(测试保护) ✅ 逐渐优雅

TDD 的关键思想:

先用测试锁定行为,再在测试保护下优化结构。

行为不变,设计持续演进。

最终结论

本示例展示了一个完整的 TDD 循环:

  • Red:明确业务行为("我要什么")
  • Green:用最小实现达成行为("先让它跑起来")
  • Refactor:在安全网下优化设计("让它更优雅")

这就是 TDD 的核心:

用测试定义行为,用代码满足行为,用重构保持代码健康。


测试替身 Test Doubles

测试替身不是"替代对象",而是 根据测试目的选择不同等级的替身

就像拍电影需要替身演员一样,测试也需要"替身"来模拟真实环境。

常见的五种是:

  • Dummy(占位)—— 就像群演,只是凑数
  • Stub(控制输入)—— 就像道具,给你想要的结果
  • Mock(验证交互)—— 就像监控,记录有没有被调用
  • Spy(捕获行为 + 部分真实逻辑)—— 就像卧底,既记录又执行
  • Fake(轻量真实实现)—— 就像仿真模型,能跑但更轻量

Dummy —— 占位,不参与逻辑

特点:

  • 只为满足方法签名(让代码能编译)
  • 不参与任何业务逻辑(什么都不做)

适用场景:

当测试不关心某参数,只需要"传进去"。就像填表格,有些字段必须填,但你不在乎填什么。

业务代码

protocol Logger {
    func log(_ message: String)
}

class UserService {
    private let logger: Logger

    init(logger: Logger) {
        self.logger = logger
    }

    func getUserName() -> String {
        return "Eki"
    }
}

Dummy:为了让代码编译

class DummyLogger: Logger {
    func log(_ message: String) {
        // 什么也不做
    }
}

测试

func test_get_user_name() {
    let service = UserService(logger: DummyLogger()) // 仅占位
    XCTAssertEqual(service.getUserName(), "Eki")
}

Stub —— 控制输入(Control Inputs)

用于为依赖提供 确定性输入

例如:

  • 网络结果("返回成功")
  • 本地存储返回值("返回这个值")
  • UUID、当前时间("固定时间")
  • 配置开关("开启/关闭")

目的:

隔离外部随机因素,让测试变得稳定。就像实验室,所有条件都可控。

业务代码

protocol UserRepository {
    func fetchName() -> String
}

class UserService {
    private let repo: UserRepository

    init(repo: UserRepository) {
        self.repo = repo
    }

    func greeting() -> String {
        return "Hello, " + repo.fetchName()
    }
}

Stub:返回固定结果

class StubUserRepository: UserRepository {
    func fetchName() -> String {
        return "Eki"
    }
}

测试

func test_greeting() {
    let service = UserService(repo: StubUserRepository())
    XCTAssertEqual(service.greeting(), "Hello, Eki")
}

Stub 的重点:我不关心依赖的行为,我只要它返回我需要的输入。就像点外卖,你不在乎怎么做的,只要结果对就行。


Mock —— 验证交互(Verify Interactions)

适用于 行为验证(Behavior Verification)

Mock 用于断言:

  • 是否被调用("调了没?")
  • 调用次数("调了几次?")
  • 调用参数是否正确("参数对不对?")

常用于:

  • SDK 回调("回调触发了吗?")
  • 第三方 API 调用("API 调了吗?")
  • 事件上报("事件上报了吗?")

业务代码

protocol Analytics {
    func track(event: String)
}

class LoginService {
    private let analytics: Analytics

    init(analytics: Analytics) {
        self.analytics = analytics
    }

    func login() {
        analytics.track(event: "login_success")
    }
}

Mock:记录被调用情况

class MockAnalytics: Analytics {
    var trackedEvents: [String] = []

    func track(event: String) {
        trackedEvents.append(event)
    }
}

测试

func test_login_tracks_event() {
    let mock = MockAnalytics()
    let service = LoginService(analytics: mock)

    service.login()

    XCTAssertEqual(mock.trackedEvents, ["login_success"])
}

Mock 的重点:行为验证,而非状态验证。就像查监控,看的是"有没有做",而不是"结果是什么"。


Spy —— 捕获行为,用于更细粒度断言

Spy 有点像"半 Mock 半真实对象":

它会:

  • 保留真实逻辑(真的执行)
  • 记录调用顺序(记下来)
  • 保存内部状态变化(状态也变)

适合复杂业务流程分析。就像装了行车记录仪的车,既正常行驶,又记录一切。

业务代码

class Counter {
    private(set) var value = 0

    func increment() {
        value += 1
    }
}

Spy:记录行为,又保持真实逻辑运行

class SpyCounter: Counter {
    private(set) var incrementCallCount = 0

    override func increment() {
        incrementCallCount += 1   // 捕捉行为
        super.increment()         // 执行真实逻辑
    }
}

测试

func test_counter_increment_twice() {
    let counter = SpyCounter()

    counter.increment()
    counter.increment()

    XCTAssertEqual(counter.value, 2)              // 真实逻辑验证
    XCTAssertEqual(counter.incrementCallCount, 2) // 行为验证
}

Spy 的重点:

Mock 的行为捕捉 + 真实实现的状态变化 两者兼得。鱼和熊掌都要。


Fake —— 轻量实现(In-Memory)

Fake 是可以"跑起来"的真实轻量实现,如:

  • 内存数据库 FakeDB(不用真的数据库)
  • 内存缓存 FakeCache(不用真的缓存)
  • 假文件系统(不用真的文件系统)

特点:

  • 快(内存操作,秒级)
  • 稳定(不受网络、IO 影响)
  • 无网络 / IO 不确定性(完全可控)
  • 行为足够真实(能真实运行)

Fake 是 TDD 中最值得推荐的测试替身。就像仿真器,能跑但更轻量。

业务代码

protocol UserDatabase {
    func save(_ name: String)
    func get() -> String?
}

class UserService {
    private let db: UserDatabase

    init(db: UserDatabase) {
        self.db = db
    }

    func update(name: String) {
        db.save(name)
    }

    func currentName() -> String? {
        db.get()
    }
}

Fake:内存数据库实现,无 IO、无延迟、可运行

class FakeUserDatabase: UserDatabase {
    private var storage: String?

    func save(_ name: String) {
        storage = name
    }

    func get() -> String? {
        storage
    }
}

测试

func test_update_and_get_user() {
    let db = FakeUserDatabase()
    let service = UserService(db: db)

    service.update(name: "Eki")

    XCTAssertEqual(service.currentName(), "Eki")
}

Fake 是 TDD 中最推荐的测试替身:

足够真实、足够快速、无外部依赖。 就像游戏里的模拟器,能玩但不会真的花钱。


测试替身总结

类型 用途描述 关键词
Dummy 占位,让代码能跑 不参与逻辑
Stub 控制输入,让依赖返回确定值 "我只要你返回这个值"
Mock 验证行为,检查调用方式 "有没有调用 / 调了几次?"
Spy 捕获行为 + 执行真实逻辑 调用次数 / 顺序
Fake 可运行的轻量实现 内存数据库、FakeCache

通过合理使用 Test Doubles,可以让测试 更快、更稳定、更清晰,并且极其适合 TDD 的 Red → Green → Refactor 循环。


各语言常用测试框架与测试替身(Test Doubles)工具

下表整理了常见语言在 TDD 实践中使用的 测试框架测试替身工具(Mock / Stub / Spy / Fake)

语言 测试框架(Testing Framework) Mock / Stub 工具 Spy 工具 Fake / 其他工具 说明
Swift / iOS XCTest Cuckoo / Mockolo / SwiftyMocky Cuckoo / 自定义 Spy 类 内存实现(In-Memory Fake)、Fake Keychain、Fake DB XCTest 不内置 Mock,需要工具或自定义实现
Kotlin / Android JUnit / Kotest Mockito / MockK Mockito.spy / MockK.spy FakeRepository / FakeDAO(手写) MockK 是最现代的 Kotlin Mock 工具
Java JUnit / TestNG Mockito / EasyMock Mockito.spy H2 内存数据库、Fake Service Java 生态 Mock 工具最丰富
JavaScript / Node.js Jest / Mocha Jest.fn() / Sinon.stub Sinon.spy / Jest.spyOn FakeTimers(Jest)、Fake FS(memfs) Jest 内置 Mock 是优势
TypeScript Jest / Vitest ts-mockito / Jest.fn Jest.spyOn Fake API、Fake Storage TS 可直接复用 JS Mock
Python unittest / pytest unittest.mock / pytest-mock mock.spy InMemory DB、Fake API Python 的 mock 工具异常强大
Ruby RSpec / Minitest RSpec Mocks RSpec spies Fake ActiveRecord / 内存实现 Ruby 社区 Mock 语法优雅
Go testing gomock / testfixtures 无原生 Spy(多为手写) Fake Repo / Fake Storage(推荐) Go 更推崇 Fake 而非 Mock
C# / .NET xUnit / NUnit Moq / NSubstitute Moq callbacks InMemory DB(EF InMemory) Moq 是最流行的 Mock 框架
PHP PHPUnit Mockery / PHPUnit Mock Mockery spies Fake repository / Fake service PHP Mock 工具有长历史
Rust Cargo test mockall 手写 Spy Fake FS、Fake Repo Rust 偏向手写替身,注重类型安全
Dart / Flutter flutter_test Mockito.dart Mockito.spy(有限) FakeAsync、Fake Storage Flutter 中 Fake 非常常用

使用建议

  • Swift / Kotlin / Java / TS / Python
    → 更适合 Mock + Spy + Stub 混合使用
  • Go / Rust
    → 社区更推荐 Fake(轻量实现),而不是 Mock
  • JS/TS
    → Jest 自带 Mock,最适合 TDD 新手
  • 跨端测试(iOS + Android + JS)
    → 推荐基于 Karate / Pact / DSL 契约测试(文件即行为)

思维工具

ABS:前提-边界-状态框架

ABS(Assumptions - Boundaries - State)让团队在讨论开始前就达成一致,从而减少无效分析。

ABS 并不是我从某本书里看到的,也不是某个咨询框架的翻版,而是我在多年工程实践、跨团队协作、做复杂决策时,用着用着慢慢总结出来的一种 适合我自己的思考方式

它还有一个源头:

几年前我重新回头看概率论,突然意识到我们日常的大多数讨论,其实都与"概率论中的条件"很像——

你需要先明确 条件是什么,推理才有意义。

否则就是在无条件地谈"概率",必然得到荒谬的结论。

而这,也正是 ABS 的底层启发。

ABS 做的事情,就是在任何分析开始之前,让我们先把三件事情说清楚:

  1. Assumptions(前提) —— 我们假设什么为真?
  2. Boundaries(边界) —— 我们讨论的范围在哪?
  3. State(状态) —— 我们当下所处的真实条件是什么?

说穿了就是一句话:

没有条件,就没有推理。没有前提,就没有讨论。没有状态,就没有策略。

这是我在工程领域中,用概率论的思维习惯,把"条件化思考"逐渐系统化后的产物。

ABS 最开始只是我进行做决策的思维工具,但在不断使用过程中,发现它对团队共识、需求对齐、方案评估都特别有效。于是我把它整理出来,形成现在的样子。


Assumptions(前提)

定义:

问题成立所依赖的基础条件。

示例:

  • 假设 iOS15+ 为最低版本
  • 假设市场增长在未来三个月维持稳定

作用:

减少隐含前提带来的误判。就像打牌前先确认规则,避免后面扯皮。


Boundaries(边界)

定义:

明确分析范围与不涵盖部分。

示例:

  • 此方案只覆盖"曝光链路"
  • 不包含结算系统
  • 仅讨论可在三个月内落地的内容

作用:

防止 Scope Creep(范围蔓延)。就像画个圈,圈内讨论,圈外暂时不管。


State(状态)

定义:

当下的运行环境、成熟度、限制因素。

示例:

  • SDK 正处于 beta 阶段
  • 团队资源正在收紧

作用:

避免跨阶段决策错误,增强情境敏感度。就像看病要先了解病情,不能乱开药。

ABS 的定位:让大家先对齐"怎么想":

ABS 不是告诉你方案对不对,它是告诉你:

我们得先在同一个坐标系里讨论,或者我们得先确认事物当前的样态,后面的推理才有意义。

但也必须强调一句:

思维工具只是工具,不是规矩。不要被工具限制我们的思考。

能帮你思考,它就是好工具;如果限制你,就把它放一边。

ABS 的价值是"帮助",不是"规范"。


确定性过程(Deterministic Process)

定义:

一种基于 规则清晰、步骤可验证、输入稳定 的流程设计。

只要前提一致,输出就可预测、可复现。

特征

  • Rule-Based:每一步都有明确规则(不能"看情况")
  • Repeatable:相同输入 → 相同结果(可复现)
  • Traceable:可回溯每个决定(有迹可循)
  • Low Variance:降低随机性(减少意外)

核心价值

  • 输出结果可控(Outcome Control)
  • 风险可预测
  • 把结果从"依赖人"变成"依赖流程"

应用场景

  • 战略落地(Strategy Execution)
  • 运营优化(Operation Excellence)
  • 质量管理(QA / QC)
  • 系统可靠性设计(System Reliability)

总结

如果说 ABS 是用来对齐"怎么想",

那确定性过程就是确保最终"怎么做"是可控的。

确定性过程关注的是:

  • 步骤清晰(第一步做什么,第二步做什么)
  • 输入稳定(同样的输入,同样的输出)
  • 结果一致(不会因为人的不同而不同)
  • 风险可预测(知道可能出什么问题)

属于偏"执行层"的方法。

用一句话解释它:

减少随机性,让事情按预期发生。

就像流水线,每一步都有标准,结果可预测。


关于工具的一些思考

工具只是工具,不能让其限制思考和影响我们解决问题的目的。

这句话很重要。我们使用工具是为了解决问题,而不是为了使用工具而使用工具。如果工具成了负担,或者限制了你的思考,那就该换一个,或者干脆不用。

但是,如果你无法下决心使用某些工具,却又明确知道这套工具能帮我们解决问题的时候,不妨先强制自己去做,因为行为本身就会改变思考模式。

这就像学游泳,你光看教程不下水,永远学不会。只有真正去游,身体才会记住那种感觉,思维模式也会随之改变。

韩愈提出"行成于思毁于随",意思是行动要经过思考,不能随意。但我认为,行成于思,更要以行正思

思考很重要,但有时候思考会陷入死循环,或者想太多反而不敢行动。这时候,先做起来,在实践中验证思考,在实践中调整思路,反而更有效。

TDD 就是这样。你可能觉得"先写测试再写代码"很别扭,但当你真正去做的时候,你会发现这种工作方式带来的好处,你的思维模式也会慢慢改变。

所以,对于工具:

  1. 不要被工具绑架:工具是来帮你的,不是来限制你的
  2. 但也不要拒绝尝试:如果明确知道工具能解决问题,不妨先强制自己用一段时间
  3. 在实践中调整:用着用着,你自然会知道哪些工具适合你,哪些不适合

TDD 在 iOS+Android 跨端行为验证中的实践示例

(以「截图 → 加密 → 上传服务器」为例)

为了让示例更贴近真实业务,我们使用一个 iOS / Android 都普遍存在的操作链路:

截图 → 加密 → 上传服务器

这个链路跨越了多个模块(系统权限、加密、网络请求),

并且多端实现完全不同,因此非常适合作为 DSL + 契约测试 的范例。

我们使用 Karate 框架进行实现。


业务行为链路(Behavior Chain)

我们用自然语言先描述一下行为:

  1. 用户触发截图
  2. SDK 获取屏幕内容(Image)
  3. 将图像进行加密(AES/Base64/混淆之类)
  4. 上传到服务器
  5. 上传成功后回调 success
  6. 上传失败则回调 failure

这就是行为链路,我们希望 iOS 和 Android 的行为一模一样。

就像两个厨师做同一道菜,虽然用的工具不同,但做出来的味道要一样。


用 DSL 定义跨端行为(Red)

首先把行为变成可执行的 DSL 契约。

Scenario: screenshot encryption upload
  Given user triggers screenshot action
  When SDK captures screen image
  And encrypts image using AES256
  And uploads encrypted image to server
  Then server should receive encrypted payload
  And SDK should fire success callback

边界情况也需要定义:

Scenario: upload fails
  Given SDK captured image
  When encryption succeeds
  But network request fails
  Then failure callback should be triggered

此时,这些 DSL 对应的 Karate 测试一定会失败(Red),

因为 iOS 与 Android 的链路行为往往不一致,例如:

  • iOS 可能使用 UIScreen.main.snapshotView
  • Android 使用 PixelCopyView.drawToBitmap()
  • iOS 可能加密原始 PNG
  • Android 可能先压缩成 JPEG 再加密
  • iOS 异步上传,Android 阻塞等待
  • 回调时序不一致

这些不一致都会导致 Karate 契约失败。就像两个厨师做菜步骤不一样,最后味道肯定不一样。


Green:让行为契约成立(最小实现)

Green 阶段不是为了写完美代码,只是为了让行为一致。

我们统一抽象一个行为模型:

iOS

class ScreenshotService {
    func capture() -> Data { ... }      // 返回 PNG 数据
}

class Encryptor {
    func encrypt(_ data: Data) -> Data { ... }
}

class Uploader {
    func upload(_ encrypted: Data, completion: (Bool)->Void) { ... }
}

class ScreenshotSDK {
    let service: ScreenshotService
    let encryptor: Encryptor
    let uploader: Uploader

    func start() {
        let raw = service.capture()
        let encrypted = encryptor.encrypt(raw)
        uploader.upload(encrypted) { success in
            if success { onSuccess() } else { onFailure() }
        }
    }
}

Android

必须保持完全相同行为链路:

class ScreenshotSDK(
    val service: ScreenshotService,
    val encryptor: Encryptor,
    val uploader: Uploader
) {
    fun start() {
        val raw = service.capture()      // ByteArray
        val encrypted = encryptor.encrypt(raw)
        uploader.upload(encrypted) { success ->
            if (success) onSuccess() else onFailure()
        }
    }
}

通过"同一行为模型",实现跨端一致的流程。

所有平台特有的实现,都封装在 Adapter 层:

  • iOSAdapterCaptureService
  • AndroidAdapterCaptureService
  • iOSAdapterUploader(URLSession)
  • AndroidAdapterUploader(OkHttp)

Karate 测试(DSL 契约)通过,即完成 Green。


Refactor:进一步提取跨端共性逻辑

Refactor 阶段的关键是:

把 iOS / Android 中重复的"行为逻辑"沉淀成带保护的抽象。

我们提取一个完全跨端共用的核心类:

class ScreenshotPipeline {
    func execute(capture: ()->Data,
                 encrypt: (Data)->Data,
                 upload: (Data)->Bool) -> Bool {
        let raw = capture()
        let encrypted = encrypt(raw)
        return upload(encrypted)
    }
}

两端都只需要注入端上 Adapter 即可。

测试保持不变,行为仍然受 DSL 契约保护。


AI 如何加速整个 TDD 流程

AI 可以极大提升 TDD 的效率,尤其在 DSL 驱动开发中。


(1) AI 自动从 PRD 生成 DSL(Red 阶段)

你给 AI 输入业务需求:

"触发截图 → 加密 → 上传 → 成功回调。"

AI 自动输出:

Scenario: screenshot encryption upload
  Given user triggers screenshot
  When capture() returns bytes
  And encrypt() transforms bytes
  And upload() sends encrypted payload
  Then success callback is fired

AI 还能自动生成:

  • 异常路径("如果网络失败怎么办?")
  • 边界情况("如果图片太大怎么办?")
  • 性能约束("必须在 3 秒内完成")
  • 并发/重复触发场景("用户快速点击两次怎么办?")

这些都是人最容易漏掉的。AI 就像个细心的助手,帮你想到各种情况。


(2) AI 自动生成 Test Skeleton(Green 之前)

AI 会根据 DSL 自动生成:

  • Karate 文件
  • XCTest / JUnit 桩测试
  • Fake Encryptor、Fake Uploader
  • 最小实现提示

例如:

class FakeUploader: Uploader {
    var lastUploaded: Data?
    var result = true
    func upload(_ encrypted: Data, completion: (Bool)->Void) {
        lastUploaded = encrypted
        completion(result)
    }
}

你不需要手写这些重复样板。AI 帮你写好了,你只需要填充业务逻辑。


(3) AI 作为"行为差异检测器"

修改 iOS / Android 任一端后,AI 会:

  • 对比 DSL 契约
  • 对比行为路径(call graph)
  • 告诉你"你的行为与契约是否一致"
  • 提醒你是否需要更新 DSL 或代码

这比传统回归测试更"智能化"和上下文敏感。就像有个代码审查员,时刻盯着你的代码。


(4) AI 协助 Refactor(自动指出抽象机会)

在 Green 后,AI 可以:

  • 识别重复逻辑(capture → encrypt → upload)
  • 建议抽象(Pipeline)
  • 自动生成接口 + 依赖注入方案
  • 指出坏味道(重复、耦合、不一致的异步处理)

Refactor 阶段将变得高效且可控。AI 帮你找到优化点,你只需要决定要不要改。


最终闭环:DSL × TDD × 契约测试 × AI

以 Screenshot → Encrypt → Upload 为例,整套方法论的闭环是:

  1. DSL 定义行为(Red) —— 先想清楚要什么
  2. Karate 执行契约,保证跨端一致性 —— 确保两端行为一样
  3. 最小实现完成 Green —— 先让它跑起来
  4. Refactor 将行为抽象成可跨端复用的 Pipeline —— 让它更优雅
  5. AI 提升整个循环效率、覆盖率、鲁棒性 —— 让整个过程更快更好

一句话总结:

行为 → DSL → 契约 → AI 参与 → 设计沉淀 → 多端一致性保证。

这是现代工程最强的 TDD 实践方式。


写在最后

TDD 不是银弹,也不是必须的。但它是一种很好的工作方式,能帮你写出更好的代码,减少返工,提升团队协作效率。

如果你还没试过 TDD,不妨先强制自己用一段时间。就像学游泳,先下水,身体会记住那种感觉。

记住:行成于思,更要以行正思。思考很重要,但实践更重要。在实践中验证思考,在实践中调整思路。

工具只是工具,不要被工具绑架,但也不要拒绝尝试。用着用着,你自然会知道哪些适合你,哪些不适合。

Let's think!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

相关阅读更多精彩内容

友情链接更多精彩内容