.NET Core TDD 前传: 编写易于测试的代码 -- 缝
有时候不是我们不想做单元测试, 而是这代码写的实在是没法测试....
举个例子, 如果一辆汽车在产出后没完成测试, 那么没人敢去驾驶它. 代码也是一样的, 如果项目未能进行该做的测试, 那么客户就不敢去使用它, 即使使用了也会遇到“车祸”.
为什么要测试/测试的好处
- 它可以尽早发现bug, 解决bug
- 它会节省开发和维护一个软件的总成本. 实际上我们在维护软件上付出的成本要远大于在开发时付出的成本. 开发的时候编写单元测试确实会增加一些成本, 但是从长远来看这些测试还是会从维护上降低软件的总成本.
- 它会促使开发者改进设计. 如果开发时先写测试或者同时写测试代码, 那么开发者会不得不仔细考虑要解决的问题, 所以会写出更好的设计, 而且无需考虑如何测试代码.
- 相当于自成文档. 因为所有的测试就是被开发软件所有期待的行为.
- 增强自信, 去除恐惧. 有时修改代码后我们就会担心这是否对现有的功能造成了破坏, 而如果单元测试覆盖了软件的重要功能的话, 那么只要测试都能通过, 那么就基本可以确信功能没被破坏.
测试从不同的角度看可以分成很多类. 我们首先应该保证好单元测试能够很好的进行, 只要单元测试能够很好的进行, 那么其它测试应该都可以很好的进行.
为什么要写易于测试的代码
再详细说一下:
在谈到软件测试的时候, 网上的文章经常举这个建造汽车的例子, 那我也拿汽车这个例子说明问题吧.
假设我们需要设计并生产一辆汽车, 可能会有两种方式:

第一种是把车设计成一个复杂的整体, 把所有需要的零件都焊到了一起, 也可以说它只有一个大零件, 就是汽车本身. 这样做的好处就是我们不必花那么多时间和精力去制作发动机, 轮胎, 车窗等等这些可替换的零件了. 这么去做是有可能把汽车的设计和生产成本降低的. 但是如果汽车被长期使用, 考虑到售后及维护, 那么成本肯定会非常高了.
如果汽车坏了, 我们无法检测是哪里出错, 因为它是一个整体, 无法对某部分进行隔离测试; 即使我们知道哪里有问题, 我们还是无法替换损坏的部分, 因为它还是一个整体...

第二种方式就是正确的方式, 我们使用可替换的零件进行设计生产, 这样就会方便测试和售后维护. 因为车里的每个零件都可以被替换, 也可以取出来单独进行测试. 如果汽车不能启动, 那么就对每个零件进行检查, 最后替换出问题的零件即可, 而无需像第一种方式那样把整个车扒开进行大修.
很明显, 正常的汽车厂商都是使用的第二种方式, 因为其具有可测试性和可维护性.
软件开发这个领域和设计汽车是很相似的, 可以像第一种方式一样开发软件, 也可以像第二种方式一样开发软件.
在现实中, 有太多的开发者使用了第一种方式, 把一大堆代码和功能都放到了一起. 而实际上开发者们应该采用第二种方式来进行代码的设计和编写, 即使在开发初期这可能会花掉更多的时间和精力.
有的时候不是开发者不想采取第二种方式, 而是花了很大力气却发现写出来的代码仍然不能很好的进行单元测试, 所以实际问题是不知道该如何写出易于测试的代码.
什么样的代码易于测试
还是汽车的例子, 如果我们怀疑汽车的电瓶坏了, 那么采用第一种方式创造的汽车就无法进行对它的“电瓶”进行单独检测, 因为是焊到一起的, 也没有可以用检测的插头等; 而采用第二种方式建造的汽车则可以把电瓶拿出来, 然后我们使用电压表等专用的仪器在隔离的情况下对其进行检测.

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

虚线就是缝.
由于有缝的存在, 所以我们可以进行隔离测试:

分别使用Test Fixture和Test 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进行依赖注入即可.
可测试的代码应该如下:


.NET Core TDD 前传: 编写易于测试的代码 -- 缝的更多相关文章
- .NET Core TDD 前传: 编写易于测试的代码 一 -- 缝
转载于: https://www.cnblogs.com/cgzl/p/9365955.html 有时候不是我们不想做单元测试, 而是这代码写的实在是没法测试.... 举个例子, 如果一辆汽车在产出后 ...
- .NET Core TDD 前传: 编写易于测试的代码 -- 全局状态
第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 第3篇, 依赖项和迪米特法则. 本文是 ...
- .NET Core TDD 前传: 编写易于测试的代码 -- 单一职责
第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 第3篇, 依赖项和迪米特法则. 第4篇 ...
- .NET Core TDD 前传: 编写易于测试的代码 -- 构建对象
该系列第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 本文是第2篇, 介绍的是如何避免在构建对象时写出不易测试的代码. 本文的概念性内 ...
- .NET Core TDD 前传: 编写易于测试的代码 -- 依赖项
第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 本文是第3篇, 讲述依赖项和迪米特法则 ...
- 新书《编写可测试的JavaScript代码 》出版,感谢支持
本书介绍 JavaScript专业开发人员必须具备的一个技能是能够编写可测试的代码.不管是创建新应用程序,还是重写遗留代码,本书都将向你展示如何为客户端和服务器编写和维护可测试的JavaScript代 ...
- 编写可测试的JavaScript代码
<编写可测试的JavaScript代码>基本信息作者: [美] Mark Ethan Trostler 托斯勒 著 译者: 徐涛出版社:人民邮电出版社ISBN:9787115373373上 ...
- 从一张图开始,谈一谈.NET Core和前后端技术的演进之路
从一张图开始,谈一谈.NET Core和前后端技术的演进之路 邹溪源,李文强,来自长沙.NET技术社区 一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和 ...
- 编写Avocado测试
编写Avocado测试 现在我们开始使用python编写Avocado测试,测试继承于avocado.Test. 基本例子 创建一个时间测试,sleeptest,测试非常简单,只是sleep一会: i ...
随机推荐
- Java中的基本类型和引用类型变量的区别
Java中的基本类型和引用类型变量的区别 学了一年多,说实话你要我说这些东西我是真说不出来是啥意思 基本类型: 基本类型自然不用说了,它的值就是一个数字,一个字符或一个布尔值. 引用类型: ...
- 记录一波由会话堵塞导致tomcat应用故障事件
一.故障基本信息 发生时间 消除时间 故障历时 故障类别 影响 2018-5-17 18:14:30 2018-05-18 08:58:15 16小时 应用故障 业务瘫痪,用户投诉 二.故障现象 AP ...
- BZOJ_2962_序列操作_线段树
Description 有一个长度为n的序列,有三个操作1.I a b c表示将[a,b]这一段区间的元素集体增加c,2.R a b表示将[a,b]区间内所有元素变成相反数,3.Q a b c表示询问 ...
- 【爆料】-《西澳大学毕业证书》UWA一模一样原件
☞西澳大学毕业证书[微/Q:2544033233◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归&a ...
- Django基础四(model和数据库)
上一篇博文学习了Django的form和template.到目前为止,我们所涉及的内容都是怎么利用Django在浏览器页面上显示内容.WEB开发除了数据的显示之外,还有数据的存储,本文的内容就是如何在 ...
- 理解图像分割中的卷积(Understand Convolution for Semantic Segmentation)
以最佳的101 layer的ResNet-DUC为基础,添加HDC,实验探究了几种变体: 无扩张卷积(no dilation):对于所有包含扩张卷积,设置r=1r=1 扩张卷积(dilation Co ...
- Python 实现文件复制、删除
Python 实现文件复制.删除 转载至:http://www.cnblogs.com/sld666666/archive/2011/01/05/1926282.html 用python实现了一个小 ...
- 深入学习MySQL事务:ACID特性的实现原理
事务是MySQL等关系型数据库区别于NoSQL的重要方面,是保证数据一致性的重要手段.本文将首先介绍MySQL事务相关的基础概念,然后介绍事务的ACID特性,并分析其实现原理. MySQL博大精深,文 ...
- Python2与Python3字符编码的区别
目录 字符编码应用之Python(掌握) 执行Python程序的三个阶段 Python2与Python3字符串类型的区别(了解) Python2 str类型 Unicode类型 Python3 字符编 ...
- ASP.NET Core 实战:构建带有版本控制的 API 接口
一.前言 在上一篇的文章中,主要是搭建了我们的开发环境,同时创建了我们的项目模板框架.在整个前后端分离的项目中,后端的 API 接口至关重要,它是前端与后端之间进行沟通的媒介,如何构建一个 “好用” ...