Skip to content

测试驱动开发

概述

测试驱动开发(TDD)的话题将分两章讨论。本章先讨论 TDD 的基础技术细节。

TDD 三法则

TDD 四纪律

TDD 要求必须遵循以下纪律:

  1. 创建测试集,方便重构,并且其可信程度达到系统可部署的水平。意即,若测试集通过,系统就可部署。
  2. 创建足够解耦、可测试、可重构的代码。
  3. 创建极短的反馈循环周期,保障代码编写工作以及稳定的节奏和产出运行。
  4. 创建相互解耦的测试代码和生产代码,以资方便地分别维护,无须在两者之间复制改动。

TDD 纪律包括三条法则。TDD 三法是 TDD 纪律的基础。

TDD 第一法则

第一法则:在编写因为缺少生产代码而必然会失败的测试之前,绝不编写生产代码。

你也许会想,没有代码怎么测试?这一认知源自人们通常认为应该写完代码再写测试。但仔细考虑就会明白,如果你能写出生产代码,那么你也能写出测试生产代码的代码。

TDD 第二法则

第二法则:只写刚好导致失败或者通不过编译的测试。编写生产代码来解决失败问题。

测试的第一行代码显然会无法通过编译,因为这行代码是用来与还不存在的代码交互的。这也意味着在编写生产代码之前,无法继续写测试代码。

TDD 第三法则

第三法则:只写刚好能解决当前测试失败问题的生产代码。测试通过后,立即写其他测试代码。

TDD 三法会把你锁在一个循环中:

  • 写一行测试代码,无法通过编译。
  • 写一行生产代码,编译成功。
  • 写一行测试代码,无法通过编译。
  • 写一两行生产代码,编译成功。
  • 再写一两行测试代码,这次可以编译成功,但断言失败。
  • 再写一两行生产代码,满足断言。

如此循环,贯穿始终。

摒弃调试

你不应该成为调试器的高手。成为调试器高手的前提是花大量时间和精力做调试,这意味着你可能花了很多时间在修复有问题的代码上。调试器用的越舒服,你就越知道自己可能做错了什么。尽可能地多花时间在写能工作的代码上,尽可能地少花时间在修复有问题的代码上。

采用 TDD 并不能保证消灭对调试器的需求,你还是时不时地会做调试。但是调试的频率会急剧下降,单次花费时间会急剧减少。

文档

遵守三法就是在为整套系统编写代码示例。测试阐释系统运行的每个小细节。测试是在最低层描述整个系统的文档。以文档的标准而言,它们几近完美。

设计中的坑

大多数人都干过写好代码后再写测试,这是普遍现象。但是这并不好玩!因为一旦这么干,在写测试的时候我们就知道系统能正常工作了。这时候写测试无非处于某种责任感或者负罪感,或者就是管理层对测试覆盖率的硬性要求。

然后,我们不可避免地遇到了不好写的测试。代码并没有设计成可测试的样子,自然很难写测试。为了写测试代码,我们不得不修改设计!这会很痛苦:可能需要花很长的时间,可能会搞坏什么东西。同时,我们又知道代码能运行!最后,我们甩手走人,在测试集里留下一个大坑。长此以往,测试集中的坑越来越多。

好的测试集里没有坑。好测试集在通过时允许你做一个决定,那就是部署。如果测试集通过,你就会有信心部署系统。

如果测试集不能给你提供这种信心,那它还有用吗?

设计

如果你先写测试,就不会写出难以调试的代码。先写测试,会逼着你设计易于测试的代码。

什么让代码难以测试?是耦合和依赖。易于测试的代码没有这些耦合与依赖。易于测试的代码本身就已解耦!

这就是 TDD 的设计。

恐惧

每个改动都会导致崩溃的风险,而且那种风险极难侦测和修复。这是一种恐惧。你害怕自己维护的代码。你害怕破坏它导致的后果。

烂代码为什么会存在?因为恐惧。没人敢动手改进它,没人敢冒险清理它。

勇气

前文提过,只要通过测试,就有信心部署系统。在清理代码时有测试在手,当你搞坏了什么东西时,测试立即会告诉你。

有了测试集,就能安全地清理代码。只要能安全地清理代码,你就能去清理代码。换言之,测试给你清理代码的勇气。

童子军军规

当我们有了可以信赖的测试集,就能遵守以下规则:

签入的代码要比你签出的代码更整洁。

想想看,每个人在签入代码前,都对代码做了一点点好事,做了一点点清理工作。没人签入更坏的代码,人人签入更好的代码。

每次动代码,代码都会变得比之前更好。代码会越来越整洁!

第四法则

重构是 TDD 的第四法则:先写一点点会失败的测试代码,随后写一点点能通过测试的生产代码,然后清理刚写的那些混乱代码。

重构是 TDD 的重要组成部分:

  • 重构是一种持续行为。每走一圈 TDD 循环,你都会做清理工作。
  • 重构不改变代码行为。测试通过后才做重构。在重构时,测试一直能通过。
  • 不需要专门留时间做重构,因为你会一直做重构。

把重构比作上完洗手间后洗手,这是我们始终做的体面行为。

基础知识

很难用文字写出有效的 TDD 范例。TDD 的节奏不好表达。想要真正体会得看到过程。

优秀软件设计者的目标是将大型和复杂的系统切分为一系列简单的小问题。程序员的工作是将这些系统切分为一行行代码。

根据本书提供的简单示例,我们可以总结出六条规则。这些规则就像是启示:

  1. 先编写测试,能逼着你写出已知道自己要写的代码。
  2. 让测试失败。让测试通过。清理代码。
  3. 别挖金子。
    1. 当刚开始尝试 TDD 时,你会急于解决较难或较有趣的问题。这种行为就叫⌈挖金子⌋。如果太早挖金子,就有可能忽略周边所有细节。我们要有意避免早期挖金子,而是专注于测试周边行为。
  4. 先写最简单、最具体、最基本,且易失败的测试。
    1. 有些测试仅仅用于迫使我们创建需要的类、函数或其他结构;有些测试非常原始,什么也没断言;有些测试会断言非常浅白的东西。这些测试常常在稍后被更为复杂的测试所替代,可以放心删除。这是我们在测试的过程中需要遵循的搭梯测试(Stairstep Tests)。因为它们就像是阶梯,让我们可以一步步增加复杂度到合适的层级。
  5. 能泛化时泛化。
    1. 通过将具体的常量放入可操作的变量来泛化。
    2. 测试越具体,代码越通用。测试设计极其重要,规避碎片化测试也极为重要。
  6. 如果代码让人感觉不对,在继续之前,先修正设计问题。
    1. 功用测试(Misplaced Responsibility)是一种设计缺陷。函数看起来要执行某种计算,但实际上不执行这种计算。计算在其他地方执行。
    2. 用小函数替代注释几乎总是好主意。

总结

使用 TDD,有如下好处:

  • 你将花更多的时间写能正常运行的代码,花更少的时间调试不能正常运行的代码。
  • 你将产出一套几近完美的低层级文档。
  • 你将产出一套测试集,使你有信心部署系统。
  • 你将创建低耦合的设计。
  • 每次签入整洁的代码。

本章学习了 TDD 的动机和基础。你大概已经晕头转向,但目前的内容还远远不够,下一章将深入讨论 TDD。

Released under the MIT License.