.NET Core TDD 前传: 编写易于测试的代码 -- 缝

有时候不是咱们不想作单元测试, 而是这代码写的实在是无法测试....数据库

举个例子, 若是一辆汽车在产出后没完成测试, 那么没人敢去驾驶它. 代码也是同样的, 若是项目未能进行该作的测试, 那么客户就不敢去使用它, 即便使用了也会遇到“车祸”. 编程

 

为何要测试/测试的好处

  • 它能够尽早发现bug, 解决bug
  • 它会节省开发和维护一个软件的总成本. 实际上咱们在维护软件上付出的成本要远大于在开发时付出的成本. 开发的时候编写单元测试确实会增长一些成本, 可是从长远来看这些测试仍是会从维护上下降软件的总成本.
  • 它会促使开发者改进设计. 若是开发时先写测试或者同时写测试代码, 那么开发者会不得不仔细考虑要解决的问题, 因此会写出更好的设计, 并且无需考虑如何测试代码.
  • 至关于自成文档. 由于全部的测试就是被开发软件全部期待的行为.
  • 加强自信, 去除恐惧. 有时修改代码后咱们就会担忧这是否对现有的功能形成了破坏, 而若是单元测试覆盖了软件的重要功能的话, 那么只要测试都能经过, 那么就基本能够确信功能没被破坏.

测试从不一样的角度看能够分红不少类. 咱们首先应该保证好单元测试可以很好的进行, 只要单元测试可以很好的进行, 那么其它测试应该均可以很好的进行. app

 

为何要写易于测试的代码

再详细说一下:框架

在谈到软件测试的时候, 网上的文章常常举这个建造汽车的例子, 那我也拿汽车这个例子说明问题吧.函数

假设咱们须要设计并生产一辆汽车, 可能会有两种方式:单元测试

第一种是把车设计成一个复杂的总体, 把全部须要的零件都焊到了一块儿, 也能够说它只有一个大零件, 就是汽车自己. 这样作的好处就是咱们没必要花那么多时间和精力去制做发动机, 轮胎, 车窗等等这些可替换的零件了. 这么去作是有可能把汽车的设计和生产成本下降的. 可是若是汽车被长期使用, 考虑到售后及维护, 那么成本确定会很是高了.测试

若是汽车坏了, 咱们没法检测是哪里出错, 由于它是一个总体, 没法对某部分进行隔离测试; 即便咱们知道哪里有问题, 咱们仍是没法替换损坏的部分, 由于它仍是一个总体...spa

 

第二种方式就是正确的方式, 咱们使用可替换的零件进行设计生产, 这样就会方便测试和售后维护. 由于车里的每一个零件均可以被替换, 也能够取出来单独进行测试. 若是汽车不能启动, 那么就对每一个零件进行检查, 最后替换出问题的零件便可, 而无需像第一种方式那样把整个车扒开进行大修.设计

很明显, 正常的汽车厂商都是使用的第二种方式, 由于其具备可测试性可维护性3d

 

软件开发这个领域和设计汽车是很类似的, 能够像第一种方式同样开发软件, 也能够像第二种方式同样开发软件.

在现实中, 有太多的开发者使用了第一种方式, 把一大堆代码和功能都放到了一块儿. 而实际上开发者们应该采用第二种方式来进行代码的设计和编写, 即便在开发初期这可能会花掉更多的时间和精力. 

有的时候不是开发者不想采起第二种方式, 而是花了很大力气却发现写出来的代码仍然不能很好的进行单元测试, 因此实际问题是不知道该如何写出易于测试的代码.

 

什么样的代码易于测试

仍是汽车的例子, 若是咱们怀疑汽车的电瓶坏了, 那么采用第一种方式创造的汽车就没法进行对它的“电瓶”进行单独检测, 由于是焊到一块儿的, 也没有能够用检测的插头等; 而采用第二种方式建造的汽车则能够把电瓶拿出来, 而后咱们使用电压表等专用的仪器在隔离的状况下对其进行检测.

第二种方式之因此能够进行隔离测试是由于它采用的是可替换零件, 也就是零件能够拿下来.

用专业的术语说就是第二种方式里有缝(seam). 在软件里, 什么是缝(seam)? 缝就是你能够在程序里替换行为的地方, 而不须要在这个地方进行修改. 或者说就是可让你的代码移除依赖项并建立出可用于隔离测试对象的地方.....我可能解释的不明白, 看图吧:

虚线就是缝.

 

因为有缝的存在, 因此咱们能够进行隔离测试:

分别使用Test FixtureTest double来替换调用类和依赖项.

而采用第一种方式的软件就没法把代码拆出来进行测试了, 由于没法替换依赖项, 没法接入到测试环境, 也就是说没法进行隔离测试了.

 

为何代码会没法进行隔离测试呢

没法测试的代码有一些特色:

  • new 关键字. 若是这部分代码里出现了new关键字, 也就是说在构造函数或方法内创造了外部资源或较复杂类型的实例, 那么测试就会很困难了. 而应该采用的作法是依赖注入.
  • 静态方法/属性调用. 静态方法会为它的调用者和它被调用时所在的类建立很紧的耦合. 使用像Math.Min(), String.Join()这些方法时是没有题的, 可是若是使用DateTime.Now, Console.Write() 那就可能会出问题了. 这时候你可能就须要使用一个包装类了.
  • 单立体 Singleton. Singleton的本质是共享状态. 可是为了隔离测试, 最好仍是避免使用singleton. 若是确实须要使用它的话, 那么在测试的时候可使用一个非Singleton的替身来进行测试, 固然, 经过依赖注入.
  • 全局共享状态, 这个应该明白
  • 引用第三方框架或外部资源. 一旦有这样的引用的话, 就没法进行隔离测试了. 咱们须要作的就是对这些东西抽象化, 把细节忽略只关心特定条件下的特定结果.

 

如何产生缝隙

  • 解藕依赖项. 在C#里, 咱们经过对接口编程而不是对实现来编程来实现这个任务. 
  • 依赖注入. 主要是采用构造函数注入.

作到这两点, 那么咱们就可使用test double(测试替身)来代替依赖项并注入到被测试类使用, 从而进行隔离测试.

 

例子

下面就是一个难以测试的例子, 这个代码并不完美, 没法展现出不可测试代码全部的特色, 可是也包含了至少两个特色:

首先它的依赖项都是new出来的, 这些依赖项就有依赖于数据库的, 因此测试的话, 咱们还须要知道数据库里面特定的数据内容..这样的结果就是测试很难完成.

其次这里用到了第三方的Mapper.Map()静态方法, 这个方法也许是通过测试的而且没有反作用的, 可是也有可能不是. 并且它形成了ProductControllerHard和Mapper类之间的紧耦合.

 

针对第一个问题, 我想都知道怎么去处理了, 就是使用接口. 我就很少介绍了.

针对第二个问题, 使用静态方法形成了紧耦合. 若是这个静态方法是咱们本身写的方法, 咱们能够对其重构, 变成实例方法. 可是若是它来自第三方库, 而且第三方库没有提供能够依赖注入使用的版本, 那么咱们本身能够写一个包装类(wrapper)来包装该方法:

可是因为这个Mapper来自AutoMapper库, 这个库提供了IMapper接口, 因此使用IMapper进行依赖注入便可.

 

可测试的代码应该以下:

相关文章
相关标签/搜索