在单元测试培训系列:(一)单元测试概念以及必要性中,我们已经说过单元测试的定义是什么,里面有提到一个很重要的概念:隔离! 是的,没有隔离就没有可测试性,也就没有单元测试。
可测试性Testability
下面我们具体解释下什么叫做可测试性Testability:
让你的代码变的更加松耦合(Loosely coupled),让类与类之间的关联性降低,降低到可以个别独立存在,如此一来便可在彼此互不影响之下完成个别的单元测试,而这些类又能组合成一个有用的应用程式。
因为单元测试要尽可能的隔离与当前方法逻辑没有关系的方法以及外部资源(I/O文件,配置文件,数据库,网络以及静态变量等),即要求每段代码在不依赖其他额外方法以及外部资源的情况下依然可以正确执行!
这么说也许你无法理解也很难想象应该如何做到,那么我们下来举个例子说明。
Code Sample
以上代码中,可以看到Testability类的ComplexCompute方法中依赖于CalcServiceWrapper类,并调用了该类的Multiply和Divide方法,最终完成了自身的功能。但这个代码就不具有很好的可测试性,因为ComplexCompute无法离开CalcServiceWrapper类而正确编译执行。那么如何重构这段代码使它可以解耦从而具有良好的可测试性呢?依赖倒置(Dependency Inversion)可以做到!
依赖倒置
依赖倒置是一种设计模式,具体的概念大家可以参见:向依赖关系宣战——依赖倒置、控制反转和依赖注入辨析, 在这里就不深入的去阐述依赖注入是怎么回事了。
其实,如果你已经仔细看完那篇文章或者你已经知晓那些知识,那么其实所谓的解耦重构也就很简单了。
依赖注入,有三种实现方式:属性注入,构造函数注入,方法参数注入;在实际项目中,属性注入是最常见的方式,我们下面就来用这种方式来对之前的代码进行解耦重构:
Code Sample
如上代码中可以看到,把ComplexCompute方法中用到的CalcServiceWrapper类以及实例化的代码去掉,而增加一个类型为ICalcService(其实也可以是CalcServiceWrapper)的公开属性,在ComplexCompute方法中通过调用该属性来实现调用逻辑。这时候我们再来看,有没有发现,CalcService.Multiply方法和CalcService.Divide方法,无论CalcService是否被赋值,被赋予任何对象并不会影响当前方法的编译通过。换句话说:ComplexCompute方法不再依赖于CalcServiceWrapper类型是否实现以及如何实现。
可能说到这里,你依然无法理解,代码被重构后解耦了,但这跟可测试性又有多大的关系呢?很遗憾的是目前因为有个很重要的概念还没能介绍,在这里只能简单的提及,那就是Mock Object,将会在后面的章节详细介绍,这里先简单带过。所谓Mock Object其实就是个假对象,但它只是模拟一个对象中某个/些方法,输入某些参数,返回一个预先设定的值,不用真的去执行代码。这样在做单元测试的时候,我们可以给Testability类实例的CalcService属性设置一个Mock Object, 然后在我们测试其中的ComplexCompute方法的时候,我们可以保证里面的用到的CalcService方法都按照我们期望并设定的方式执行(返回的值),从而实现:我们测试Testability类中的ComplexCompute方法的时候,不会再担心CalcServiceWraper类的实现是否存在错误,或者发生什么问题,因为在ComlexCompute中执行的是Mock Object,而不是真正的CalcServiceWraper对象。我们的单元测试于是就只是测试ComplexCompute方法,不再涉及其他方法!
非可测试性场景Untestable Scene
上一段提到的是依赖倒置是最正规的符合Testability的代码模式,但实际应用中,我们可能无法做到完全很好的执行,但有几种场景我们是必须要特别注意并避免的。
1. 静态变量
静态变量是非常不符合Testability要求的(但在处理遗留系统Legacy System时会有特别的用处,后文会提及)。
首先来看静态变量,因为静态变量是全局性的,而且是保持有状态的。在单元测试培训系列:(一)单元测试概念以及必要性中,我们有说过,单元测试是测试的最小单位,必须可信任的,可重复执行的。因此,若使用静态变量,就会导致破坏单元测试的可重复执行性,并且可能干扰到别的单元测试方法。
例如:在方法A中改变了全局静态变量StaticVariableA的值,而在方法B中又要去尝试读取这个静态变量的值,那么在单元测试执行的过程中,若执行方法A和方法B的单元测试方法顺序不同,或执行多次很可能每次结果都会不同,这样明显会导致我们的单元测试结果不再可信。因此,我们应该尽量避免静态变量的使用。
2. 静态方法
静态方法的可测试性不强的原因倒不是因为静态方法自身有什么太大的问题,而是在编写单元测试的过程中受制于工具框架的限制:目前的开源Mocking Framework中都不支持对静态方法进行模拟,若大量使用静态方法,会在编写单元测试的时候遇到无法解耦的问题。当然利用一些商业Mocking Framework,如TypeMock,Moles等可以规避这个问题。
除了静态方法外,因为同样的开源Mocking Framework项目的限制,sealed类也是可测试性很差的代码。具体原因,我们放到后面的Mock Object章节一起介绍。
3. 直接依赖外部环境
一般来说,单元测试是要求代码尽可能不直接依赖于I/O文件接口,数据库,网络环境甚至系统环境(如时间等),但实际上一个项目总会有一部分代码无可避免的需要去访问这些外部资源,但我们可以尽量的控制这些代码集中在较小的范围内,并通过提取接口的方式使其他代码和这些直接访问外部资源的代码之间解耦。
因此,泛滥的在代码中随意使用访问外部资源的代码是非常Untestable的设计,例如:在程序中到处都是访问数据库的代码,DataContext出现在每层代码中;到处充满依赖于ASP.NET或者WCF环境的代码(HttpContext, HttpRequest等)。
我们没办法实现对边界代码的单元测试,只能通过集成测试来检验,例如对数据库访问层代码的测试;对网络访问层代码的测试等。但我们必须尽可能的防止这些边界代码的扩散,要尽可能的使他们被隔离起来。这个问题因为涉及面很广,暂不展开说明,在接下来的篇幅中会专门针对Entity Framework和ASP.NET MVC的Testability进行优化分析。