TDD:它是什么、测试原则和方法论

作者: IT Sectr 发布日期: 2026-04-09 阅读时间: 9 分钟

Test-Driven Development (TDD) — 是一种在代码实现之前编写测试的开发方法论。开发人员首先以失败的测试形式阐述预期行为,然后编写最少的代码使其通过,之后重构结果。根据 Martin Fowler (2023) 的说法,TDD 不是一种测试技术——它是一种设计技术,能够约束架构并减少代码编写阶段的缺陷数量。

要点

  • TDD — 测试在实现之前而非之后编写的开发方法论
  • Red-Green-Refactor 循环 — TDD 的基础:红色测试、绿色测试、重构
  • JUnitMockito — Android 开发中 TDD 的主要工具
  • 代码覆盖率 在 TDD 项目中经常超过 90%,这得益于 “测试优先” 的纪律
  • 重构 无需担心破坏功能 — TDD 方法的关键优势

什么是 TDD?

Test-Driven Development — 是一种软件开发实践,其中自动化测试决定生产代码的编写。与传统方法(先编写代码,然后测试)不同,TDD 颠倒了顺序:先编写测试,然后编写通过此测试的代码。

TDD 的奠基人是 Kent Beck,他在 20 世纪 90 年代末在 Extreme Programming (XP) 方法论的框架内阐述了这一实践。在 “Test-Driven Development: By Example” (2002) 一书中,Beck 描述了 TDD 的五条成为经典的规则:在生产代码之前编写测试,编写恰好足够通过测试的代码,以及在每个循环之后进行重构。

TDD 的关键原则

第一个原则 — 测试定义接口。开发人员在考虑组件如何实现之前被迫考虑组件将如何使用。这从一开始就形成了干净的 API。

TDD 作为设计技术

第二个原则 — 最小化实现。当测试编写完成后,开发人员编写恰好足够通过测试的生产代码——不多写一行。这可以防止过早的抽象和过度复杂化,Martin Fowler 称之为 Speculative Generality

TDD 与普通测试的区别

TDD 与 “事后” 测试之间的关键区别 — 顺序的纪律。在 TDD 中,测试不仅检查代码——它还引导其结构。根据 Microsoft Research (Nagappan et al., 2008) 的研究,应用 TDD 的团队与传统方法的团队相比,缺陷密度降低了 40–90%。

Red-Green-Refactor 循环

Red-Green-Refactor 循环 — 是为每个新测试重复的三步序列。Red:编写一个不通过的测试。Green:编写最少的代码使测试通过。Refactor:在不改变其行为的情况下改进代码。

Red 阶段:编写失败的测试

开发人员编写一个检查尚未实现功能的测试。在这个阶段,测试应该 失败——这确认了测试确实在检查某些东西。在 Android 开发环境中,JUnit 5 框架为失败的测试显示红色指示,这为该阶段命名。

kotlin
class CalculatorTest {
    fun testAddition() {
        val result = Calculator().add(2, 3)
        Assertions.assertEquals(5, result)
    }
}

Green 阶段:最小化实现

在这个阶段,编写足够通过测试的最少生产代码。没有冗余——只有绿色指示所需的内容。如果实现可以是常量——那就让它成为常量。重构将在下一步进行,当新测试出现时。

kotlin
class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }
}

Refactor 阶段:无风险改进

绿色测试是 重构 的保障。开发人员可以重写实现、优化性能或改进可读性,确信测试将立即检测到与预期行为的任何偏差。在 Android 移动开发中,这个阶段对于提取公共接口和减少代码重复尤为重要。

TDD 在移动开发中的优势

在移动项目中应用 TDD 可带来可衡量的优势,这些优势得到了学术研究和领先开发工作室实践的证实。

降低缺陷密度

IBM (Bhat & Nagappan, 2006) 对四个工业项目的研究表明,使用 TDD 的团队与类似传统工作的团队相比,缺陷减少了 40%。对于移动开发,在 Google Play 发布后修复错误的成本远高于代码编写阶段,这一指标至关重要。

通过测试记录代码

按 TDD 编写的测试充当 API 的 活文档。进入项目的开发人员可以阅读测试并了解每个组件应该如何被使用。在团队高流动性的情况下——移动工作室的典型问题——这尤其有价值。

自信的重构

超过 90% 的测试覆盖代码使开发人员能够进行重构而不用担心破坏任何东西。Google 在 “Software Engineering at Google” (2020) 一书中将测试覆盖称为关键因素,它使得在拥有数百万行代码的项目中保持代码库整洁成为可能。

TDD 的工具和框架

移动开发中的 TDD 生态系统包括用于单元测试、模拟和 UI 组件检查的工具——既适用于 Android,也适用于 iOS。

工具平台用途
JUnit 5Android (Kotlin/Java)单元测试的基础框架
MockitoAndroid创建模拟对象和验证调用
MockKAndroid (Kotlin)使用 Kotlin-first 语法的模拟,支持协程
TurbineAndroid测试 Kotlin Flow 和响应式流
XCTestiOS (Swift)标准测试框架

为 Android 选择框架

对于 Kotlin 上的 Android 项目,标准堆栈包括 JUnit 5 + MockK。MockK 优于 Mockito,因为它支持 Kotlin 的一级函数——密封类、协程和挂起函数,无需额外配置。

iOS 工具

在 iOS 开发中,TDD 通过 XCTest 实现——Apple 的内置框架,提供断言、测试类以及通过 Xcode Server 或 GitHub Actions 与 CI/CD 的集成。iOS 上的模拟使用 Cuckoo 和 OHHTTPStubs 库。

Kotlin 中的 TDD 代码示例

让我们看看 Android 上 Kotlin 中一个真实的 TDD 场景——测试用户仓库。首先编写测试,然后——通过此测试的实现。

步骤 1:UserRepository 的测试

kotlin
class UserRepositoryTest {
    private val api = mockk<UserApi>()
    private val dao = mockk<UserDao>()
    private val repo = UserRepository(api, dao)

    fun `when api returns user then cache and emit`() = runTest {
        val user = User(1, "Alice")
        coEvery { api.getUser(1) } returns user
        every { dao.insert(user) } returns Unit

        val result = repo.getUser(1)

        assertEquals(user, result)
        verify { dao.insert(user) }
    }
}

步骤 2:最小化实现

kotlin
class UserRepository(
    private val api: UserApi,
    private val dao: UserDao
) {
    suspend fun getUser(id: Int): User {
        val user = api.getUser(id)
        dao.insert(user)
        return user
    }
}

步骤 3:带有离线模式的缓存测试

通过第一个测试后,我们添加第二个——检查 网络错误 时的行为。现在测试规定,当 API 失败时,仓库应从缓存返回数据。

kotlin
fun `when api fails then return cached user`() = runTest {
    val cached = User(1, "Cached Alice")
    coEvery { api.getUser(1) } throws IOException()
    every { dao.getById(1) } returns cached

    val result = repo.getUser(1)

    assertEquals(cached, result)
}

实施 TDD 时的常见错误

过渡到 TDD 伴随着典型错误,这些错误可能会使方法论的所有优势都付诸东流。理解这些陷阱有助于团队更有效地实施该实践。

测试太大

第一个也是最常见的反模式 — 在一个测试中 测试过多的功能。测试应该只检查一个断言。如果测试失败,开发人员应该知道确切是哪里出了问题,而无需额外的调试。

忽略红色阶段

第二个错误 — 编写一开始就通过的测试。如果测试从未至少变红一次,就没有信心它确实在检查任何东西。规则:永远不要相信你从未见过失败的测试。

跳过重构

第三个典型错误 — 停留在绿色阶段。重构 不是可选的,而是循环的强制阶段。没有它,代码库会退化,测试变得脆弱,TDD 的优势也会丧失。

  • 测试实现而非行为 — 测试绑定到细节,在每次重构时都会断裂
  • 缺少边缘情况的测试 — 空列表、null 值、边界条件未被覆盖
  • 忽视测试速度 — 慢速测试会减慢反馈循环并扼杀 TDD 纪律

常见问题

TDD — 是测试技术还是设计技术?

TDD 首先是一种设计技术,而不是测试技术。TDD 中的测试扮演规范的角色:它们在组件实现之前定义其 API。Kent Beck 本人称 TDD 为 “设计的纪律,而非测试的纪律”。

掌握 TDD 需要多长时间?

根据 Microsoft Research 的研究,团队需要 3 到 6 个月的持续练习才能让 TDD 成为习惯。前 2–3 周生产效率下降 15–30%,但在适应之后,由于调试时间的减少,会恢复到初始水平或超过它。

TDD 适用于 UI 组件吗?

是的,但有局限性。对于 UI 逻辑(ViewModel、State),TDD 可以直接应用。对于可视化组件(Compose UI、SwiftUI Views),快照测试(snapshot testing)补充 TDD,但不能替代它。建议将业务逻辑和展示分离。

TDD 可以应用于遗留项目吗?

对于 遗留代码,建议使用 “表征测试”(characterization tests)策略——即测试编写在现有行为上,然后重构代码。这种方法在 Michael Feathers 的 “Working Effectively with Legacy Code” (2004) 一书中有所描述,允许逐步实施 TDD。

TDD 如何与 Clean Architecture 结合?

TDD 和 Clean Architecture 相互增强。干净架构要求层之间有清晰的边界,而 TDD 迫使开发人员通过测试来设计这些边界。领域层使用模拟依赖关系隔离测试,数据层——通过集成测试。

总结

  • TDD — 测试在实现之前编写的开发方法论,形成干净的 API 并引导架构
  • Red-Green-Refactor 循环 — TDD 的基本单元:失败的测试 → 最小化实现 → 重构
  • 根据 IBM 和 Microsoft Research 的研究,应用 TDD 可将缺陷密度降低 40–90%
  • Android 的主要工具:JUnit 5MockK、用于 Flow 的 Turbine
  • 在 Kotlin 项目中,由于对协程和密封类的支持,MockK 优于 Mockito
  • 典型错误:测试太大、跳过红色阶段、忽视重构
  • 推荐的实施策略 — 逐步推进,从领域层和新功能开始,而不是试图一次性覆盖所有遗留代码

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读