@xuemingdeng
2017-01-12T07:13:11.000000Z
字数 2120
阅读 535
2016年3月,来自Stripe的Nelson Elhage在他的博客上与我们分享了他在实际工作当中探索总结出来的一些关于可测性代码的设计原则。Nelson在文章中阐述了代码可测性的重要性和它为软件质量提升所带来的好处,并列出了可测性代码的一些特点。以下内容翻译自Nelson的博文。
在设计软件项目时,我们总是会面临很多结构性方面的问题。比如,哪些是核心抽象组件?它们之间是如何进行交互的?
在这篇文章里,我将会介绍我所发现的一个探索性的设计原则,它或许在某种程度上可以回答上述的问题:
为了可测性而优化代码
具体地说,就是在你准备写新代码时,你要先做好设计,要考虑它们与系统其它组件之间的关系。此时,你要问自己一些问题:“我将如何测试这些代码?我将如何在尽量不考虑运行环境因素的前提下编写自动测试用例来验证代码的正确性?”如果你无法回答好这些问题,那么请重新设计你的组件或接口。
我将从两个方面来讨论这个原则的好处。
你会得到良好的测试
这个好处或许是显而易见的:如果你希望让你的代码得到良好的测试,那么到最后一般都会如愿。
不过,我认为还有更多的东西值得一说。
首先,我要强调代码测试的重要性。测试让检验代码变更的过程变得很简单:只要运行测试就可以了。没必要再搭建一个复杂的开发环境,也没必要与系统进行各种复杂的交互,只要敲入简单的make test命令,然后等待结果。这样最起码可以知道你的代码变更是否按照你所期待的那样运行,并且知道它们是否对重要功能造成破坏。
在成熟的大型软件系统里,在做出变更时最困难的部分不是变更本身,而是如何保证所做的变更不会对系统的重要功能造成影响。好的测试可以为此提供最基本的保障,因此测试是一种富有生产力的改进措施。
其次,为了让测试所带来的好处得以体现,你需要运行测试,最起码要运行那些能够覆盖代码变更的测试用例,而且要尽快运行,以便保证开发的速度。
为了让这种特性成为一种系统标准,需要有良好的单元测试。人们对“单元测试”、“功能测试”、“集成测试”和其它各种测试之间的边界的认识极为混乱。在这里,我所说的“单元测试”是指如何使用一些特定的模块或组件,这些模块或组件具有较少的对其它组件的依赖或对其它代码的调用。
有时候你也需要集成测试,集成测试以端到端的方式使用应用程序,以便发现组件间交互的微妙细节。不过,单元测试能够给我们带来更多的好处:
好的单元测试并非可遇不可求。如果没有好的逻辑“单元”就没有好的单元测试:那些包含小粒度且良好定义的接口的代码很容易进行单元测试。
这些逻辑单元是良好测试的基础,而测试反过来为项目提供了快速的反馈。而要找出这些逻辑单元,只要在写代码时记住这个准则。
你会得到更好的代码
在写代码时要时刻想着如何测试代码会带来更好的代码!易于测试的代码有几个特点:
基于不可变数据的纯函数代码
基于不可变数据结构的纯函数代码非常易于测试:你只需要指定一些成双的输入和输出。通过像QuickCheck这样的工具可以很容易地进行模糊测试,因为输入一般都很容易描述。
包含良好定义的接口的小模块
如果代码包含了小型的、良好定义的接口,我们可以为它编写黑盒测试来测试接口的契约,而无需关心模块内部的细节或系统的其它部分。
IO和计算的分离
一般来说,IO比一般的代码要难于测试。因此,对于可测试的系统来说,需要把IO从代码逻辑里分离出来,进行单独的测试。
显式声明的依赖
把数据库名字隐藏在内部的代码要比把数据库名字作为参数传入的代码来得更难于测试。对于后者来说,你可以使用不同的参数,每次测试都可以使用不同的新数据库,或者一次使用多个线程,总之可以根据你的需要来进行测试。
可测试代码的依赖一般是显式且参数化了的,而不是隐式且不可变的。
我发现具有良好结构的代码也有这些特点!当我们以测试为辅来架构一个系统,最后一般都会得到一个更好的系统。
更进一步说,通过测试可以间接地获得这些结果,我们可以让它们变为现实。对于一个软件系统来说,什么样才算是正确的模块、接口和结构,这个问题或许没有定论。但在面对“如何让代码变得更易于测试”这个问题时,我们就可以相对容易地做出选择。
结论
对于一个复杂的软件系统来说,没有什么比稳定地发生变更来得更重要,而高质量的自动化测试是达成这种目标最重要的工具之一。好的测试不是可遇不可求的,也不是靠蛮力能够获得的:它们是通过设计得来的,因为应用程序赋予了它们这种可能性。
当你在开发软件时,时常问自己“我将如何测试软件的准确性”,并且愿意为了达成这个目标对软件进行良好的设计。作为回报,你将得到一个具有良好结构的系统,你也会因此倍感自信。
本文已获得原作者翻译授权,查看原文:Design for Testability