很多人在开发过程中都强调测试驱动开发,单元测试,代码测试覆盖率。那么为什么大家要强调这些?这些工作非做不可么? 其实并非绝对。不论是驱动测试开发,还是代码测试覆盖率,本质上都只是方法,而不是目的。人们的真正的目的,是编写出优秀的,高质量的具有可维护性的,能够很好扩展的代码。
问题来了。
什么是具有可测试的代码?
所谓具有可测试的代码,是指能够很轻松的执行各种测试的代码。
具有可测试性的代码有什么特点?
1. 控制性。
控制性是指测试者给在被测试的软件提供固定的输入数据的方便程度。换句话说就是软件本身接受定义明确的参数,并且这些参数可由测试者灵活的传入,软件在接受到这些参数后通过一系列运算返回固定的结果。任何软件都应该清楚的表明自己需要什么参数,以及将会生成什么返回值。此外任何软件都应该应该抽象它的依赖,譬如参数或底层模块,并为外部调用者提供随意注入的方式。当然软件代码本身应该清晰,整洁,目标明确。
2.可见性。
可见性是指测试者观察正在测试的软件的当前状态以及它所产生的任何输出的能力。换句话说就是软件应该将内部运算的状态(一般是指错误状态)和输出结果清晰明确的告知测试者。可见性一般都是通过方法执行后验证后置条件完成。
验证后置条件与契约式设计有关。所谓的契约式设计,是指把组件之间的交互描述成契约,权利和义务得到明确的表达和强制实施。在.net环境下,可以通过.net Framework4新增的Code Contracts库创建软件契约。
3.简约性。
一般而言,简约性对任何系统在任何情况下都是一个正面的属性,测试毫无疑问也不例外。简单和极其内聚的组件非常适合测试,因为他们暴露出来的方法少,需要的测试也就少,而需要的测试越少,就越能做得可靠,快速。
编写可测试性代码与编写测试的关系
首先,编写可测试性代码与编写测试有本质区别。可测试性是一个特征,代表着更好的代码质量,是更容易扩展和维护的代码。而测试是流程,它的目标是验证代码是否满足人们的预期,验证代码能够按照我们的要求毫无差错的进行工作。
其次,二者有很强的关联性。如果说可测试性的代码是目的,那么编写测试就是通向目的的重要方法。
如何执行得到可测试的代码和程序
啰嗦了半天,终于到正题了。那么如何得到编写具有可测试性的代码和程序呢?
1.坚持面向对象编码原则。
编码原则多年以来,已被无数人强调过无数遍了,但是我还是要提及一下,因为短短的几行原则,真真正正是软件编码的金科玉律,不二良言。
- 单一责任原则
- 开放/封闭原则
- 里氏代换原则
- 接口分离原则
- 依赖反转原则
具体的解释,我在此不再赘述。但是不得不多提几句的是,原则的使用,切忌照本宣科。
对象建模并不容易,而且也不是精确科学。原则的存在大多数情况下是告诉我们做事的方式,为我们提供正确的方向。建模的重点是要找到正确的原则组合。如果将原则全面照本宣科的进行实施,这些原则会引导我们持续重构,直到我们突然发现在超过某个零界点后工作量继续增加,而得到的好处却开始减少,甚至得不偿失。
2.坚持编码建议。
同样是老生常谈,在此稍微提及一下,不做进一步解释。
- KISS(keep it simple,stupid)
- YAGNI(you are not gonna need it)
- DRY(don‘t repeat yourself)
- TDA(Tell don‘t ask)
同样的,这些建议也要在项目中酌情使用,照本宣科只会违背初衷。
3.使用设计模式
21中设计模式,我在此不做赘述,有大把的资料和文章对Gof进行了详细描述和解读。
我在此说明一下,我对设计模式的使用方式的理解。
设计模式不是必须执行命令,不应该武断的解读,它也不是灵丹妙药,不可能突然出现拯救项目于水火。我们不能为了使用设计模式而使用设计模式,不是我们选择一个设计模式,并把它运用到项目中。正确的方式应该是:我们遇到一个问题,然后把问题匹配到设计模式的一种。
当我们不知道如何匹配时,我们应该运用面向对象设计原则,不停的优化我们的代码,对其进行抽象,泛化。不久之后,我们就会发现我们的解决方案已经无限的接近于某一个设计模式了,然后我们欣然的选择这个模式,并利用其改造我们的代码。这个方法屡试不爽。为什么会这样呢?因为设计模式本身,就是无数前辈大牛的抽象过后的解决方案啊。
4.坚持测试驱动开发。
回到了我们之前提到的可测试性代码与测试之间的关系了。我们通过编写测试,得到可测试的代码,写测试的目标是为了得到好代码,而不是好测试。在编写代码之前,先通过讨论,得到功能点的输入输出,并提前编写相关的测试代码,然后在测试代码的约束下,得到更加高内聚低耦合的代码设计,进而完成相关的功能编码。
编写了测试,我们仅仅完成了测试驱动开发的一部分。测试仅仅是基础工程,我们后期可以依托测试,进行持续的重构,而丝毫不用担心破坏了原有的代码逻辑和返回结果。重构,才是坚持测试驱动开发的核心部分。而持续的重构,最终我们得到的就是设计良好的代码,还附带一堆高代码覆盖率的测试作为奖励。
具体的测试方法,包括不限于:单元测试,集成测试,验收测试。我们还可以使用各种仿冒对象和虚拟对象来简化测试,在此不再赘述。
参考引用:
《Microsoft.NET企业级应用架构设计》【意】Dino Esposito