less than 1 minute read

前言

程序员圈子流传着一个关于测试的段子:

每个程序员在修改代码时都希望有测试,而在写代码时,都不想写测试。

希望有测试,是因为测试给我们带来安全感。不想写测试,理由很多,觉得麻烦,生产代码都写不完写什么测试,团队也不要求写测试。

对于今天的程序员来说,尤其是在测试资源紧缩的情况下,写测试就是程序员本职工作的一部分。如果你连测试都做不好,那你对代码的信心从何而来呢?如何保证代码的高质量?所以,我们要克服种种困难,去写测试。

什么是TDD

TDD 是敏捷软件开发(Agile software development)中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么生产代码。TDD 是极限编程(Extreme Programming)的核心实践和基石底座。它的主要推动者是 Kent Beck。

什么是极限?

极限的意思就是将常识性的原理和实践用到了极致。 如果写测试是有意义的,那么所有人都应该始终进行测试(单元测试),甚至包括客户(功能测试)。 如果代码评审(Code Review)是有意义的,那么我们有能给个始终进行代码评审(结对编程)。

TDD可以有三层含义:

  • Test-Driven Development 测试驱动开发,即先测试,再写生产代码。
  • Task-Driven Development 任务驱动开发,将业务需求进行分解,拆分为独立的子任务。
  • Test-Driven Design 测试驱动设计,提供测试保障,调整代码设计更加容易。

为什么要学习TDD

每当想到要先写测试,再写生产代码,给人的感觉是,效率一定会很低,毕竟要编写的代码数量,要比只写生产代码要多。然而恰恰相反,TDD最直接的收益,就是可以提高开发者的工程效能,主要体现在几个方面。

想清楚

软件开发的第一步是理解需求,开发者经常会陷入拿到需求,直接开始编码的情况。TDD的最重要的好处就是,让开发者再编写生产代码前,通过一个一个的测试用例先想清楚需求,以及需要什么样的测试才能验证生产代码符合预期。一旦发现理解有出入,立刻和产品、客户确认需求,调整测试用例。这会有效的减少返工,带来全局效率上的提升。

聚焦

此外,人本来是单线程的生物,尤其是工程师,每次只处理一个任务时效率最高。TDD要求,每次只能编写刚好让当前单测通过的生产代码,和当前任务无关的代码,一行也不准写。这从心理上有效减轻负担,降低认知负载,编码时就会更自信、勇敢。

验证性测试优于定位性测试

软件工程的效能提升,不仅是开发功能的效能,还包含发现问题、定位问题和修复问题的效能。我们日常在编写完生产代码后,在验证的过程中发现不符合预期,会使用debug、arthas等工具去定位问题,因为不知道哪里出现问题,只能跟着调试器一步一步往下走,这种测试方式称为「定位性测试」,非常低效,且不能自动化。TDD能够帮助我们,快速对生产代码进行验证,通过断言提前发现bug,减少后期调试的时间。同时按照TDD的流程,最终会形成可重复、自动化的测试,获得更高的工程效能

重构

最后,TDD开发完生产代码后,天然就有了测试保证,那么识别坏味道,进行代码重构就会更加顺畅,可以避免工程后期陷入“改不动、不敢改”的困境。

什么情况下适合TDD

  • 需求清晰的业务项目。

  • 遗留系统的重构。

什么情况下不适合TDD

频繁的需求变动,在使用TDD的话可能会让流程更加繁琐,从而阻碍开发进度。

- 往可测试性极差的遗留系统添加功能,使用TDD可能得不偿失。

为什么TDD落地难

TDD基本都听过,但现实中采用的团队很少,WHY?

  • 主观体验只可意会,难以言传。 会 TDD 的人对其中的妙处有着非常强烈的主观体验,而不会 TDD 的人,则体会不到。所以最佳的学习路径,就是由熟练者来指导新人。

  • 缺少实战练习。TDD并不是一项技术,而是一种软件开发的思维方式。思维方式的转变,绝不是看看文章和视频就能掌握的,需要大量的刻意练习,细细品味其中的奥秘。

  • 不会任务拆解。TDD最重要的第一步,是任务拆解,大部分初学者卡在了这一步。

  • 不会写有效的测试。不会通过测试表达业务用例。

  • 不会重构。不知道什么是坏味道,不知道什么是整洁的代码。即便看了《重构》那本书,也不一定掌握常用的重构技巧。

如何TDD

最经典的莫过于红-绿-重构这样的一个循环。 

1. 先写一个失败的测试(得到一个红灯)。 1. 用最快的方式,不惜犯下一切罪恶,让这个测试通过(红 ->绿的过程)。 1. 识别坏味道,用常用的手法重构代码。

TDD详细流程

1.写一个新测试,并运行所有的测试。 2.因为新测试对应的生产代码还没有实现,自然测试无法通过。 3.开始编写生产代码,并快速让这个测试通过。 4.当所有的测试都通过,且发现有明显的坏味道时,开始重构。 5.重构后运行所有测试,测试通过后,写新的测试,开启下一次循环。如果重构导致大量测试失败,恢复代码并考虑更细粒度的重构。

这里有几个常见的疑问

生产代码还没写,那么运行测试时,一定会失败。为什么明知道会失败,还要运行一次?

- 管理你的预期。你需要确切的知道,测试按照预期的结果失败了,你不可能在为这个测试去写测试。运行一下得到红灯,让你确切的知道,你的目标就是编写相应的生产代码,让这个测试通过。

- 当已经有足够多的生产代码时,运行新的测试也不一定失败。新写的测试对应的生产代码可能已经实现了,只是你不自知,这时你需要调整你的测试。

为什么要在后期再进行重构,而不是一开始就设计出优秀的代码?

  • 关注点分离,让你在实现需求时只关注在一个问题上。在 红 -> 绿的阶段,我们不关心代码结构,只关注怎么快速实现功能,不需要过多考虑需要引入什么设计模式等去优化代码结构。而在后续重构的过程中,因为测试的存在,我们可以时刻检查功能是否依旧正确,同时将关注点转移到“怎么让代码变得更好”上去。

  • 遵从简单设计,避免过度设计。你是否YY过潜在的业务需求,写出看似扩展性良好的代码。然后实际上没有任何新的业务场景,利用了你做的扩展性设计。

  • TDD并不强制要求、也不反对提前设计,你可以在写测试之前进行提前设计,也可以在编写测试的时候进行设计。关键是要找到一种方式来确保你的代码能够正常工作,并且符合你的需求。

TDD三原则

Kent Beck在他的《测试驱动开发》中提到,TDD要遵守以下三条原则。

原则一

You are not allowed to write any production code unless it is to make a failing unit test pass.

翻译:在动手写或者修改任何一行生产代码前,都应该有所目的,除非是为了重构。而这个目的、需求,都应该通过测试案例描述出来,当透过测试案例来完成这个「代办」需求时,自然会得到一个红灯,修改任何生产代码都是以让红灯变绿为目标。

解读:本质上是「先目标再路径」还是「先路径再目标」,是「先射箭再画靶」还是先「画靶再射箭」。TDD要求开发人员先有验证生产代码的测试(目标),再通过编写生成代码,让这个测试通过(路径),以便得到功能正确的代码(do right thing),而不仅是能动的代码(do thing right)。

原则二

You are not allowed to write any more of a unit test than is sufficient to fail;and compilation failures are failures.

翻译:当你已经有一个失败的测试案例时,要做的应该是修改生产代码来通过这个测试案例,而不是增加更多的失败案例,编译失败也算失败。

解读:核心在于聚焦,当你写了多个失败的测试时,会增加开发者的压力。每次只写一个测试,并让这个测试通过。

原则三

You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

翻译:每一行生产代码,目的应该都只是为了让眼前的红灯变绿灯。和这个测试案例无关的代码,一行也不准写。

解读:不能写超过当前测试范围的生产代码,因为不知道这些代码是否可以工作。

第一个TDD案例——FizzBuzz

FizzBuzz是一个小游戏,非常适合拿来展示TDD的软件开发流程。游戏的内容是:

你是一名体育老师,在某次课距离下课还有五分钟时,你决定搞一个游戏。此时有100名学生在上课。游戏的规则是:

  1. 让所有学生排成一队,然后按顺序报数。

  2. 学生报数时,如果所报数字是(3)的倍数,那么不能说该数字,而要说Fizz;如果所报数字是(5)的倍数,那么要说Buzz;如果所报数字是(7)的倍数,那么要说Whizz。
  3. 学生报数时,如果所报数字同时是两个特殊数的倍数情况下,也要特殊处理,比如3和5的倍数,那么不能说该数字,而是要说FizzBuzz,以此类推。如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz。
  4. 学生报数时,如果所报数字包含了(3),那么也不能说该数字,而是要说相应的单词,比如要报(13)的同学应该说Fizz。
  5. 如果数字中包含了(3),那么忽略规则2和规则3,比如要报30的同学只报Fizz,不报FizzBuzz。

强烈建议在看视频前,自行动手TDD一下

引用&拓展阅读

  • 《测试驱动开发》

  • 极客时间 -《TDD项目实战70讲》

  • 《匠艺整洁之道》

  • 《有效的单元测试》

  • 程序袁帅的个人空间-程序袁帅个人主页-哔哩哔哩视频

  • 从红灯变绿灯的过程

  • 深度解读 - TDD(测试驱动开发)