译者的话
测试驱动(TDD)的思想早有耳闻,但是我们都只是听说过它有很多好处,却很少有人实践。其实我们对其优势并没有全面的了解。最近我阅读了许多这方面非常优秀的文章。如果有时间我会多翻译一些发出来。这是单元测试系列的第二篇译文。
原文链接:Twelve Benefits of Writing Unit Tests First
系列第一篇译文可查看:打桩(Stubbing), Mocking 和服务虚拟化的差异
正文
为什么程序员讨厌写单元测试?为什么他们甚至更讨厌在写代码之前先写单元测试?你不需要回答。我已经听到过很多的借口。这些是反问句。我有一套理论,然而,真正的原因是什么呢?
大多数软件开发人员从来没有认真地尝试过 测试先行(test-first) 的开发方式。或者,如果他们尝试过了,他们并没有在一个支持性的环境中这样做。但是更多的是前者。所以他们会给出这样的托辞:“我们没有时间写单元测试。”或者:“单元测试不能使你的代码刀枪不入。”他们的反应是出于恐惧而非独立思考的结果。他们试图找出让生活不幸的原因而不是让他们的生活更好。巧合的是,偶尔你会发现这些开发人员中有的人,当你摆出事实给他看时,他自己都站不住脚了。看起来很有趣。他做这一切,从来没有收回他对先写测试的看法。这是一种邪恶的哲学。
就像 Kent Beck 在 《Test Driven Development By Example》中解释说,测试先行由三步组成:
- 红(Red) —— 写一个表达你将如何使用代码和你需要它去做的事情的测试。这个测试通不过(因为还没有实现),在很多UI中以红条(red bar)的形式展现。
- 绿(Green) —— 写出足够的代码以让测试通过,但是除此之外不能写更多。如果你需要写更多的代码,例如,检查错误,那么先写另一个测试以表达这个特性。现在,继续写足够的代码以让测试通过。
- 重构 —— 清除代码以移除冗余代码和改善设计。然后重新运行测试以确保你没有破坏任何东西。
重复这个过程直到你完成任务。这是一个难以置信的简单的过程。但是为什么开发者恐惧它呢?因为它需要人们在开发软件的方式上做一次根本范式的转换。
你如何解决软件问题?在学校里他们怎么教你处理的?你要做的第一件事情是什么?你在想如何解决它。你会问,“我需要写什么代码来生成一个解决方案?”那是后面的。这是你必须做的第一件事,事实上这也是他们在学校里说的,但是根据我的经验,这更多的是一种口头禅而非真正的服务。你要问的第一个问题不是“我将要写什么代码?”而是“我怎么知道我已经解决了这个问题?”
我们一直被教导着事先假设说我们已经知道了如何说明我们的解决方案是否可行。这不是一个问题。像下流的行为一样,我们一看到就会知道。我们相信,在写代码之前,我们不需要去想它需要做什么。这个信念如此根深蒂固,我们大多数人都很难改变。当然,像一个我这样的捣乱份子,这个改变只不过是一个明智且可行的实验而已。
这里为那些可以实现这一飞跃的人提供了几个你将会体验到的好处。所有这些我都经历过了。但是别只是听我在这里说,你自己试试看。
- 单元测试证明你的代码真的可用。 这意味着你的代码很少有bug。不,单元测试不能替代系统测试和验收测试。但是它们确实补充了它。更少的bug,让软件质量保证(Software Quality Assurance, SQA)变得更好。
- 你得到一个低层级的回归测试套件。你可以随时重复执行并且看到不止是出现了什么错误,而且能知道bug出在哪里。很多团队将执行单元测试做为常规编译的一部分。这是一种低成本的在进入系统性测试编译之前抓出bug的方式。
- 你能在没有破坏设计的情况下改进它。 这其实是上面提到的第三步(重构)的一部分。因此,测试先行的代码通常不需要重构。我曾接手过一些非常神经质的系统,像一个精神错乱的人一样,你都不能理清它们。拥有单元测试,你可以做非常强大的重构,这些重构可以让你从大多数精神错乱系统的挑战中脱身。
- 写单元测试让写代码变得更加有趣。 你知道你的代码需要做什么。然后你让它去做。甚至如果你没有一个正常工作的系统,你也可以看到你的代码真正运行起来并且可以正常工作的样子。你得到一种非常强烈的“我成功了!”的成就感。现在每分钟重复执行一次。如果你想变得非常嗨,为你的工作感到骄傲,以此激励持续前进,尽管尝试测试先行的开发模式吧!
- 它们展现出实质性的工作进展。 你不必为了让系统的所有部分都组装到一起等上一个月时间。你可以展示当前进度,甚至不需要有一个真正运行的系统。你不仅能说你已经把代码写好了,而且你可以实实在在地展示效果。当然,这是另一个传统编程教我们忽略的目标。“完成”不意味着你把代码写完了然后提交。“完成”意味着代码在系统中没有bug地真正跑起来了。运行单元测试是让我们更接近后者的一步。
- 单元测试是示例代码的一部分。 我们都遇到过一些我们不知道怎么用的方法和类库。第一个我们要找的地方就是示例代码。示例代码就是文档。但是我们并不总是能在内部代码中找到示例代码。所以我们接着通过源码或者系统的其他地方详细查找有没有示例代码。因为张三,那个写这些代码的人已经离职了,所以我们没办法去问他这些方法和类是怎么运作的。但是单元测试就是文档。所以当我们记不清如何使用类 Foo 的时候,在单元测试里找找看。
- 测试先行迫使你在写代码之前做了计划。 先写测试迫使你在写代码之前思考你的设计和它必须达到什么结果。这不仅使你专注,而且能得到更好的设计。
- 测试先行减少 bug 的成本。 Bug 越早发现就越容易修复。Bug 较晚被发现通常是由多个变化引起的,我们并不知道哪一个变化引发的这个 bug。所以,首先我们必须尽快找出bug。因此,我们必须刷新关于代码应该如何工作的记忆,因为我们有好几个月没见过它了。最终我们有了足够的理解以提出一个方案。任何可以减少写下bug和检测到它之间的时间的事情看起来都是明显的胜利。我们认为我们自己足够幸运,在将代码移交给 SQA 或者客户的之前,能够在几天内找到bug。但是几分钟内就能抓住这些bug感觉如何?这就是测试先行能完成的事情。
- 它甚至比代码检测更好。 他们说,代码检测比测试更好,因为使用代码检测来探测和修复bug比测试更划算。当代码移交之后,修复bug将更加昂贵。我们越早探测到bug并修复它,就越容易、越省事和越好。这就是代码检查(code review)的优势:代码检测在几天内捕获更多的bug,而不是几个月。但是测试先行可以在几分钟内捕获一些bug而不是几天。它甚至比代码检测更省事。
- 它实际上消除了编程人员的灵感枯竭问题。 有没有想过接下来要写什么声明?就像作家的文思枯竭一样,程序员的灵感枯竭也是一个实实在在的问题。但是测试先行将代码的结构化部分系统化,让你可以专注于创造性的部分。你可能会受困于如何测试下一部分或者如何让当前的测试通过,但是你永远不会为下一步要做什么的问题感到困惑。
- 单元测试造就更好的设计。 测试一小部分代码迫使你定义这些代码要负责什么。如果你可以容易地做到这点,就意味着代码的职责是定义良好的。因此,它具有高内聚的特性。如果你可以对你的代码进行单元测试,那么就意味着你可以像绑定到测试一样容易地将它与系统的其他部分进行绑定。所以,它与它周围的其他部分是低耦合的。高内聚低耦合是优秀的、可维护的设计的定义。那些容易进行单元测试的代码也是易于维护的。
- 它比没有测试地写代码更快! 或者换种方式来说,除非你真的需要代码正常工作,否则跳过单元测试是更快的。我们在代码上花费的大多数工夫花在了将它提交到源码仓库之后修复它。但是测试先行通过允许我们从一开始就获得更多正确的信息,并使错误更容易修复,消除了许多的浪费。
尽管有这么多好处,很多软件开发者还是会继续坚持他们的老路。如果你是你的组织中的流程布道者,你可能会发现你自己与其中的一些人对立。祝你一切顺利。记住,人们不会为了一些他们想要或者听起来很棒的东西买单,只有当他们感到绝望并且饥渴难耐时才会买账。我希望你可以从这个列表中得到一些对你的事业有帮助的东西。
然而,如果如果你是前者中的一员,那么你就是那些不愿意设计好软件的粗俗的程序员之一……好吧,我真的很同情你。
其他引用: