阅读测试驱动开发 (TDD)的相关书籍之后的一些感想

70 阅读7分钟

前言

最近被各种代码重构、BUG修复搞得焦头烂额,让我深刻意识到缺乏系统性的代码测试是多么愚蠢的一种做法,尤其是当一些只由自己维护的代码开放给多人开发之后,你永远都不知道某个功能是被怎样给破坏掉的。

一开始工具脚本的需求还是简单明了的,维护人只有我自己,而且只会在Windows平台使用,因此我选择了bat脚本(优点是所见即所得),当后续拓展到linux时,又补充了shell的写法,而随着Mac平台的接入,忍受不了Mac和Linux语法差异的我最终决定改为Python。

在工具脚本经历了三次重构,一次脚本语言的更换和两波维护人员的变动之后,其本体已经是臃肿不堪,涵盖的需求也是数以百计,到了似乎需要第四次重构的地步,此时我开始渐渐恐慌起重构,因为工具用的人太多了,涉及到的任务也太多了。

也就在那一刻,我意识到自己努力的方向有问题,随着一次又一次的重构,我渐渐的对于自己写的代码失去了信心,同时对于同事提交的MR也不放心起来(毕竟出问题大部分情况下还是要找我去解决),这可不是一个健康的信号,因此我决定先把优化代码框架放一边,将原本以为不重要的测试框架搭起来。

入门书籍

因为想要了解TDD,因此找了几本入门的书籍来先了解一下TDD的思想:

  1. 《测试驱动开发:入门、实战与进阶》

  2. 《Python测试驱动开发:使用Django、Selenium和JavaScript进行Web编程(第2版) 》

精彩摘要

有时候读完一本书,总是忍不住惊叹于作者笔下那闪亮的思想,恨自己为何不早些接触到它们:

  1. 专注于具体需求,编写产品代码只是为了让无法通过的测试得以通过,只在测试能够通过的前提下重构代码。
  2. 简洁为本,避开无谓之事,直白而清晰比精巧更重要,整洁而不杂乱的代码。
  3. 测试先行是一种动力,促使我们把复杂的问题及早暴露出来。
    • 如果我们对开发的功能定义不够明确,或者理解有偏差,很难写出良好的测试。
    • 打消你多余的念头,别再根据自己虚构或臆想出来的需求去编写复杂的代码
  4. 让开发者对代码更有信心,这样的一套测试能够帮助我们避开回归故障 (regression failure)
  5. TDD的开发哲学:TDD并不是为了测试代码而测试代码,其目的在于通过测试设计出更好的、结构更佳的代码。

    如果TDD只是为了测试而测试,那么测试的阶段放在写代码前和写代码后就无关紧要。

  6. 我们的目的是设计出更好的软件,而测试是促进该过程顺利执行的一种手段。TDD最后形成的单元测试也只是锦上添花,主要成果在于有了一套简洁的方案。
  7. 有一条经常提起的编程原则,叫作DRY原则,也就是Don't Repeat Yourself(别重复你自己〔已经写过的代码〕)。
  8. 我们一定要让依赖关系保持单向,这样产品代码才不会以任何方式依赖测试代码,因而也就不会让程序在接受测试与不接受测试的时候表现出不同的行为。
  9. 在消除重复测试的过程中,为了让决策过程更加客观,可以根据以下三条原则来判断:
    • 如果删掉其中一个测试,代码覆盖度(code coverage) 会不会有所变化?
    • 其中某个测试是否用来验证一种相当重要的 边界状况(edge case)
    • 这些测试是否各自拥有独特的价值,能够充当 “活文档”(living documentation)
  10. 当需求增加但功能重复时,采取 TPP(Transformation Priority Premise,变换优先论) 策略,也就是说,我们肯定不会在代码里已有的if结构下面再续写else分支,或者再往里面嵌套更深的if结构了,而是会改用一种新的数据结构来实现。
  11. 代码是否具备良好的形象:
    • 循环复杂度:某块代码的循环复杂度,等于其中的循环数与分支数之和再加1。McCabe把上限设为10,并且务实地说这是个“合理(而并非万能)的上限”。
    • 耦合度:耦合度分为输入耦合度(afferent coupling)与输出耦合度(efferent coupling)。
      • 输入耦合度:指的是有多少个其他模块依赖这个模块。
      • 输出耦合度:指的是这个模块依赖其他多少个模块。
      • 不稳定程度:输出耦合度/(输出耦合度+输入耦合度)
    • 精简程度
  12. 代码是否确切地实现了目标:
    • 内聚度:内聚度是衡量某模块内的代码是否彼此相关(relatedness)的一种指标。模块的内聚度高意味着该模块中的代码(方法、类或包)表示的是一个统一的概念〔而不是一系列松散的概念〕。
      • 最应该追求的一种层次叫作功能内聚(func-tional cohesion),这是指模块中的各个部分全都是为了实现某一项明确的任务而编写的。
      • 水平最低的内聚叫作偶然内聚(coincidental cohesion),这是指模块中的各个部分只是随意放在了一起,它们之间并没有共同的目标要实现
    • 完备度:我们的代码有没有把它该做的事情全都做完?
  13. 在编写代码的过程中有没有其他路可走:
    • 按照某种编程语言的惯用格式来书写代码,能够确保我们总是遵守最小惊讶原则(principle of least surprise,也叫作最不吃惊原则)
      • 人是系统的一部分。设计应该与用户的体验、期望以及思维模式相匹配。
    • 得墨忒尔定律(Law of Demeter)提倡编写低耦合、高内聚的代码。
      • 按照得墨忒尔定律写出来的代码总是会形成比较低调(也就是“害羞”)的模块,这种模块不会随便跟与之无关的其他模块“搭腔”

感想

  • 早期的我还在为仅实现需求所需的代码的做法而感到惭愧,认为自己没有远见,缺乏大局观。现在想来,认知还是太少了,当你无法搞清楚一个产品真正的需求时,总会陷入还能更好的焦虑之中。
  • TDD的流程我没有放入摘要之中,它的做法是:首先编写一个失败的单元测试,其次编写刚刚够用的生产代码,让测试通过,最后花时间清理代码。我认为这种做法仅适用于就对语言和产品熟练的人使用
    • 比如你对Html、CSS和JS的用法很不熟练,却又想做一个属于自己的个人博客,那么TDD的做法对于开发者来说就像是在兴趣路上又填了一堵墙,它默认你已经对这门语言基本了解,才能做到编写出一个可以被解决的测试来作为一个功能开发的前提。
  • 如果TDD只是为了测试而测试,那么测试的阶段放在写代码前和写代码后就无关紧要。虽然上面的摘抄有十几条,但是让我醍醐灌顶的只有这一条。一开始我对于产品开发的想法就是遇到问题,就添加一个测试来防止问题再次复现,可是这样一来,测试就不是对于功能展开的,而是对于问题展开的,它无法做到让我在重构时依旧对代码保持信心,反而要多加一层考虑测试的代码是否会出问题的心智负担。