《程序员修炼之道:从小工到专家》第六章:当你编码时(2)

四、易于测试的代码

我们需要在一开始就把可测试性构建进软件中,并且把各个部分连接在一起之前对每个部分进行彻底的测试。

单元测试

在隔离状态下对每个模块进行测试,目的是检验其行为。

单元测试是对模块进行演练的代码,在典型情况下,单元测试将建立某种人工环境,然后调用被测试模块中的例程。然后,它根据已知的值,或是同一测试先前返回的结果对返回的结果进行检查。

随后我们把模块装进完整的系统中,使用同样的单元测试设施把系统当做整体进行测试。

但是在此之前,我们需要决定在单元测试中测试什么。

针对合约进行测试

我们喜欢把单元测试视为针对合约的测试(参见按合约设计)。我们想要编写测试用例,确保给定的但愿遵守其合约。这将告诉我们两件事情:代码是否符合合约,以及合约的含义是否与我们认为的一样。我们想要通过广泛的测试用例与边界条件,测试模块是否实现了它允诺的功能。

如果测试模块依赖于其他模块,该如何对组合进行测试呢?首先测试模块的子组件,一旦子模块得到了检验,就可以测试模块自身。比如我们有一个使用LinkedList和Sort的模块A,那么按照顺序,我们会先全面测试LinkedList的合约和Sort的合约,然后测试A的合约。

我们为什么要这么“费事”?最重要的是我们不想制造“定时炸弹”——呆在周围不被人注意,却在项目后期的尴尬时刻爆炸的东西。通过强调针对合约进行测试,我们可以设法尽可能避免那些“下游的灾难”。

为测试而设计

当你设计模块、或者是单个例程时,应该既设计其合约,也设计测试该合约的代码。通过设计能够通过测试、并履行其合约的代码,你可以仔细考虑边界条件和其他非如此便不会发现的问题。没有什么修正错误的方法比从一开始就避免发生错误更好。

编写单元测试

模块的单元测试不应该仍在源码树的某个遥远的角落里,它必须放置在方便的地方,要记住,如果你不容易找到它,也就不会使用它。

通过测试代码易于被找到,你是在给使用你代码的开发者提供了两样无价的资源:

  • 一些例子,说明怎样使用你的模块的所有功能;
  • 用以构建回归测试、以验证未来对代码的任何改动是否正确的一种手段。

但只提供单元测试还不够,还必须运行它们,并且经常运行它们。

使用测试装备

因为我们通常都会编写大量的测试代码,并进行大量的测试,我们要让自己的生活更容易一些,为项目开发标准的测试装备。

测试装备可以处理一些常用操作,比如记录状态、分析输出是否符合预期的结果、以及选择和运行测试。

在面向对象的语言和环境中,可以创建一个提供这些常用操作的基类。各个测试对它进行继承,并增加专用的测试代码。

不管你采用的技术是什么,测试装备都应该具有以下功能:

  • 用以指定设置与清理(setup and cleanup)的标准途径
  • 用以选择个别或所有可用测试的方法
  • 分析输出是否是预期(或意外)结果的手段
  • 标准化的故障报告形式

测试应该是可以组合的;也就是说测试可以由子组件的子测试组合到任意深度。通过这一特性,我们可以使用同样的工具、同样轻松地测试系统的选定部分或整个系统。

在测试过程中我们可以临时创建一些特定函数,比如print语句等,但是在调试会话的最后,需要使即兴测试正式化。不要把测试随便扔掉,把它们加入到单元测试中。

构建测试窗口

即使是最好的测试集也不大可能找出所有的bug,这就意味着,一旦某个软件部署之后,常常需要对其进行测试——在现实世界的数据正流过它的血脉时。

含有跟踪消息的日志文件就是这样一种机制。日志消息的格式应该正规、一致。

了解运行中的代码的内部状况的另一种机制是“热键”序列。按下特定的键组合,就会弹出一个诊断控制窗口,显示状态消息等信息。

对于更大、更复杂的服务器代码,提供其操作的内部视图的一种漂亮技术是使用内建的Web服务器。

测试文化

你编写的所有软件都将进行测试——如果不是由你和你的团队测试,那就是由最终用户测试——所以你最好计划好对其进行彻底的测试。

测试你的软件,否则你的用户就得测试。

五、邪恶的向导

开发者为了短期内开发大型应用,往往借助向导,比如Microsoft Visual C++会自动为你生成超过1200行代码。

但是使用别人设计的向导代码并不能帮助你成为专家。这实际上是靠巧合编程,向导就是一条单行单——它们为你制作代码,然后就走了。如果出了问题,还是要自己来修改。

我们并不是反对向导,相反我们在代码生成器中专门讨论了如何编写自己的向导,如果你真的使用向导,却不理解它制作出的所有代码,你就无法控制自己的应用,你没有能力维护它,而且在调试时会遇到很大的困难。

不要使用你不理解的向导代码

学院君

学院君 has written 548 articles

资深PHP工程师,Laravel学院院长