开篇:上一篇我们学习基本的单元测试基础知识和入门实例。但是,如果我们要测试的方法依赖于一个外部资源,如文件系统、数据库、Web服务或者其他难以控制的东西,那又该如何编写测试呢?为了解决这些问题,我们需要创建测试存根、伪对象及模拟对象。这一篇中我们会开始接触这些核心技术,借助存根破除依赖,使用模拟对象进行交互测试,使用隔离框架支持适应未来和可用性的功能。
1.入门
2.核心技术
3.测试代码
一、破除依赖-存根
1.1 为何使用存根?
当我们要测试的对象依赖另一个你无法控制(或者还未实现)的对象,这个对象可能是Web服务、系统时间、线程调度或者很多其他东西。
那么重要的问题来了:你的测试代码不能控制这个依赖的对象向你的代码返回什么值,也不能控制它的行为(例如你想摸你一个异常)。
因此,这种情况下你可以使用存根。
1.2 存根简介
(1)外部依赖项
一个外部依赖项是系统中的一个对象,被测试代码与这个对象发生交互,但你不能控制这个对象。(常见的外部依赖项包括:文件系统、线程、内存以及时间等)
(2)存根
一个存根(Stub)是对系统中存在的一个依赖项(或者协作者)的可控制的替代物。通过使用存根,你在测试代码时无需直接处理这个依赖项。
1.3 发现项目中的外部依赖
继续上一篇中的LogAn案例,假设我们的IsValidLogFilename方法会首先读取配置文件,如果配置文件说支持这个扩展名,就返回true:
public bool IsValidLogFileName(string fileName) { // 读取配置文件 // 如果配置文件说支持这个扩展名,则返回true }
那么问题来了:一旦测试依赖于文件系统,我们进行的就是集成测试,会带来所有与集成测试相关的问题—运行速度较慢,需要配置,一次测试多个内容等。
换句话说,尽管代码本身的逻辑是完全正确的,但是这种依赖可能导致测试失败。
1.4 避免项目中的直接依赖
想要破除直接依赖,可以参考以下两个步骤:
(1)找到被测试对象使用的外部接口或者API;
(2)把这个接口的底层实现替换成你能控制的东西;
对于我们的LogAn项目,我们要做到替代实例不会访问文件系统,这样便破除了文件系统的依赖性。因此,我们可以引入一个间接层来避免对文件系统的直接依赖。访问文件系统的代码被隔离在一个FileExtensionManager类中,这个类之后将会被一个存根类替代,如下图所示:
在上图中,我们引入了存根 ExtensionManagerStub 破除依赖,现在我们得代码不应该知道也不会关心它使用的扩展管理器的内部实现。
1.5 重构代码提高可测试性
有两类打破依赖的重构方法,二者相互依赖,他们被称为A型和B型重构。
(1)A型 把具体类抽象成接口或委托;
下面我们实践抽取接口将底层实现变为可替换的,继续上述的IsValidLogFileName方法。
Step1.我们将和文件系统打交道的代码分离到一个单独的类中,以便将来在代码中替换带对这个类的调用。
①使用抽取出的类
public bool IsValidLogFileName(string fileName) { FileExtensionManager manager = new FileExtensionManager(); return manager.IsValid(fileName); }