测试驱动开发

测试驱动开发是一个很”古老”的程序开发方法,然而由于国内开发流程的问题——即开发人员负责功能的测试,导致这么好的一项技术没有在国内推广。

红-绿-重构

测试驱动开发的主要过程是: 红 —> 绿 -> 重构

TDD

  1. 先写一个失败的单元测试。即我们并没有实现这个方法,但是已经有了这个方法的测试。
  2. 让测试通过。实现简单的代码来保证测试通过,就算我们用一些作弊的方法也是可以的。我们写的是功能代码,那么我们应该提交代码,因为我们已经实现了这个功能。
  3. 重构,并改进功能代码,让它变得更加合理。

TDD 有助于我们将问题分解成更小的部分,再一点点的添加我们所需要的业务代码。随着这个过程的不断进行,我们会发现我们已经接近完成我们的功能代码了。并且到了最后,我们会发现我们的代码都会被测试到。

虽然说起来很简单,但是真正实现起来并不是那么容易。于我而言我只会在我自己造的一些轮子中使用 TDD。因为这个花费大量的时间,通常来说测试代码和功能代码的比例可能是1:1,或者是2:1等等。在自己创建的一些个人应用,如博客中,我不需要与其他人 Share 我的 Content。由于我使用的是第三方框架,框架本身的测试已经足够多,并且没有复杂的逻辑,我就没有对我的博客写测试。而在我写的一些框架里,我就会尽量保证足够高的测试覆盖率,并且在适当的时候会去 TDD。

通常来说对于单元测试我会采用 TDD 的方式来进行,但是功能测试仍会选择在最后添加进去。主要的缘由是:在写 UI 的过程中,元素会发生变化。这一点和我们在写 Unit 的时候,有很大的区别。div + class 会使得我们思考问题的方式发生变化,我们需要去点击某个元素,并观察某个元素发生的变化。而多数时候,我们很难把握好一个页面最后的样子。

不得不说明的一点是,TDD 需要你对测试比较了解后,才容易使用它。从个人的感受来说,TDD 在一开始是一件很难的事。

测试先行

对于写测试的人来说,测试先行有点难以理解,而对于不写测试的人来说,就更难以理解。这里假定你已经开始写测试了,因为对于不写测试的人来说,写测试就是一件难以理解的事。既然我们都要写测试,那么为什么我们就不能先写测试呢?或者说为什么后写测试存在一些问题?

依据 J.Timothy King 所总结的《测试先行的12个好处》:

  1. 测试可证明你的代码是可以解决问题的
  2. 一面写单元测试,一面写实现代码,这样感觉更有兴趣
  3. 单元测试也可以用于演示代码
  4. 会让你在写代码之前做好计划
  5. 它降低了 Bug 修复的成本
  6. 可以得到一个底层模块的回归测试工具
  7. 可以在不改变现有功能的基础上继续改进你的设计
  8. 可以用于展示开发的进度
  9. 它真实的为程序员消除了工作上的很多障碍
  10. 单元测试也可以让你更好的设计
  11. 单元测试比代码审查的效果还要好
  12. 它比直接写代码的效率更高

但是在我个人的感觉里,比较喜欢的是: 写出可以测试的函数。这是一个一直困扰着我的难题,特别是当我的代码里存在很多条件的时候,在后期我编写的时候,难度越来越大。当我只有一个简单的 IF-ELSE 的时候,我的代码测试起来也很简单:

  1. if (hour < 18) {
  2. greeting = "Good day";
  3. } else {
  4. greeting = "Good evening";
  5. }

而当我有复杂的业务逻辑时,后写测试就会变成一场恶梦:

  1. if (EchoesWorks.isObject(words)) {
  2. var nextTime = that.parser.parseTime(that.data.times)[currentSlide + 1];
  3. if (that.time < nextTime && words.length > 1) {
  4. var length = words.length;
  5. var currentTime = that.parser.parseTime(that.data.times)[currentSlide];
  6. var time = nextTime - currentTime;
  7. var average = time / length * 1000;
  8. var i = 0;
  9. document.querySelector('words').innerHTML = words[i].word;
  10. timerWord = setInterval(function () {
  11. i++;
  12. if (i - 1 === length) {
  13. clearInterval(timerWord);
  14. } else {
  15. document.querySelector('words').innerHTML = words[i].word;
  16. }
  17. }, average);
  18. }
  19. return timerWord;
  20. } else {
  21. document.querySelector('words').innerHTML = words;
  22. }

我们需要重新理清业务的逻辑,再依据这些逻辑来编写测试代码。而当我们已经忘记具体的业务逻辑时,我们已然无法写出测试。

思考

通常在我的理解下,TDD 是可有可无的。既然我知道了我要实现的大部分功能,而且我也知道如何实现。与此同时,对 Code Smell 也保持着警惕、要保证功能被测试覆盖。那么,总的来说 TDD 带来的价值并不大。

然而,在当前这种情况下,我知道我想要的功能,但是我并不理解其深层次的功能。我需要花费大量的时候来理解,它为什么是这样的,需要先有一些脚本来知道它是怎么工作的。TDD 变显得很有价值,换句话来说,在现有的情况下,TDD 对于我们不了解的一些事情,可以驱动出更多的开发。毕竟在我们完成测试脚本之后,我们也会发现这些测试脚本成为了代码的一部分。

在这种理想的情况下,我们为什么不 TDD 呢?

参考资料

J.Timothy King 《Twelve Benefits of Writing Unit Tests First》