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

有时候不是我们不想做单元测试, 而是这代码写的实在是没法测试....

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

 

为什么要测试/测试的好处

  • 它可以尽早发现bug, 解决bug

  • 它会节省开发和维护一个软件的总成本. 实际上我们在维护软件上付出的成本要远大于在开发时付出的成本. 开发的时候编写单元测试确实会增加一些成本, 但是从长远来看这些测试还是会从维护上降低软件的总成本.

  • 它会促使开发者改进设计. 如果开发时先写测试或者同时写测试代码, 那么开发者会不得不仔细考虑要解决的问题, 所以会写出更好的设计, 而且无需考虑如何测试代码.

  • 相当于自成文档. 因为所有的测试就是被开发软件所有期待的行为.

  • 增强自信, 去除恐惧. 有时修改代码后我们就会担心这是否对现有的功能造成了破坏, 而如果单元测试覆盖了软件的重要功能的话, 那么只要测试都能通过, 那么就基本可以确信功能没被破坏.

测试从不同的角度看可以分成很多类. 我们首先应该保证好单元测试能够很好的进行, 只要单元测试能够很好的进行, 那么其它测试应该都可以很好的进行. 

 

为什么要写易于测试的代码

再详细说一下:

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

假设我们需要设计并生产一辆汽车, 可能会有两种方式:

640?wx_fmt=jpeg

第一种是把车设计成一个复杂的整体, 把所有需要的零件都焊到了一起, 也可以说它只有一个大零件, 就是汽车本身. 这样做的好处就是我们不必花那么多时间和精力去制作发动机, 轮胎, 车窗等等这些可替换的零件了. 这么去做是有可能把汽车的设计和生产成本降低的. 但是如果汽车被长期使用, 考虑到售后及维护, 那么成本肯定会非常高了.

如果汽车坏了, 我们无法检测是哪里出错, 因为它是一个整体, 无法对某部分进行隔离测试; 即使我们知道哪里有问题, 我们还是无法替换损坏的部分, 因为它还是一个整体...

 

640?wx_fmt=png

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

很明显, 正常的汽车厂商都是使用的第二种方式, 因为其具有可测试性可维护性

 

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

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

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

 

什么样的代码易于测试

还是汽车的例子, 如果我们怀疑汽车的电瓶坏了, 那么采用第一种方式创造的汽车就无法进行对它的“电瓶”进行单独检测, 因为是焊到一起的, 也没有可以用检测的插头等; 而采用第二种方式建造的汽车则可以把电瓶拿出来, 然后我们使用电压表等专用的仪器在隔离的情况下对其进行检测.

640?wx_fmt=png

第二种方式之所以可以进行隔离测试是因为它采用的是可替换零件, 也就是零件可以拿下来.

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

640?wx_fmt=png

虚线就是缝.

 

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

640?wx_fmt=png

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

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

 

为什么代码会无法进行隔离测试呢

无法测试的代码有一些特点:

  • new 关键字. 如果这部分代码里出现了new关键字, 也就是说在构造函数或方法内创造了外部资源或较复杂类型的实例, 那么测试就会很困难了. 而应该采用的做法是依赖注入.

  • 静态方法/属性调用. 静态方法会为它的调用者和它被调用时所在的类创建很紧的耦合. 使用像Math.Min(), String.Join()这些方法时是没有题的, 但是如果使用DateTime.Now, Console.Write() 那就可能会出问题了. 这时候你可能就需要使用一个包装类了.

  • 单立体 Singleton. Singleton的本质是共享状态. 但是为了隔离测试, 最好还是避免使用singleton. 如果确实需要使用它的话, 那么在测试的时候可以使用一个非Singleton的替身来进行测试, 当然, 通过依赖注入.

  • 全局共享状态, 这个应该明白

  • 引用第三方框架或外部资源. 一旦有这样的引用的话, 就无法进行隔离测试了. 我们需要做的就是对这些东西抽象化, 把细节忽略只关心特定条件下的特定结果.

 

如何产生缝隙

  • 解藕依赖项. 在C#里, 我们通过对接口编程而不是对实现来编程来实现这个任务. 

  • 依赖注入. 主要是采用构造函数注入.

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

 

例子

下面就是一个难以测试的例子, 这个代码并不完美, 无法展示出不可测试代码所有的特点, 但是也包含了至少两个特点:

640?wx_fmt=png

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

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

 

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

针对第二个问题, 使用静态方法造成了紧耦合. 如果这个静态方法是我们自己写的方法, 我们可以对其重构, 变成实例方法. 但是如果它来自第三方库, 并且第三方库没有提供可以依赖注入使用的版本, 那么我们自己可以写一个包装类(wrapper)来包装该方法:

640?wx_fmt=png

但是由于这个Mapper来自AutoMapper库, 这个库提供了IMapper接口, 所以使用IMapper进行依赖注入即可.

 

可测试的代码应该如下:

640?wx_fmt=png

640?wx_fmt=png

原文地址:https://www.cnblogs.com/cgzl/p/9365955.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/320659.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数位DP】好数(jzoj 1521)

好数 jzoj 1521 题目大意&#xff1a; 定义好数为转换为二进制后&#xff0c;有至少三个连续的位相同的数&#xff0c;现在要求一个范围内的好数个数 样例输入 0 16样例输出 5数据范围限制 0 < Low < UP < 2147483647 提示 提示&#xff1a; 对于50%测试&…

P4294-[WC2008]游览计划【斯坦纳树】

正题 题目链接:https://www.luogu.com.cn/problem/P4294 题目大意 n∗mn*mn∗m的网格&#xff0c;每个格子的修建费用不同&#xff0c;要求修建费用最小连接所有关键点。 解题思路 设fs,i,jf_{s,i,j}fs,i,j​表示目前连接关键点状态为sss&#xff0c;在(i,j)(i,j)(i,j)这个位…

「LibreOJ NOIP Round #1」旅游路线

「LibreOJ NOIP Round #1」旅游路线 题目链接 做法&#xff1a; 首先肯定要预处理些东西&#xff0c;来使单词询问达到\(o(logn)\)或者\(o(1)\)的复杂度&#xff0c;又因为距离这个东西的范围太大&#xff0c;我们考虑预处理一些费用相关的东西。考虑\(dp[s][j]\)表示从s出发花…

【招聘(北京)】东方国信 工业互联网

从工业领域的业务出发&#xff0c;结合现有的物联网、大数据、云计算等技术进行体系化建设。自主研发的智能检测传感器、自主开发的部署在生产单位的专家系统、自主设计通讯交互协议、自主开发的工业物联网通讯框架、自主开发的分布式存储中间件、自主研发的机理模型&#xff0…

纪中C组模拟赛总结(2019.7.5)

成绩&#xff1a; rankrankrank是有算其他dalaodalaodalao的 rankrankranknamenamenamescorescorescoreT1T1T1T2T2T2T3T3T3T4T4T4888lyflyflyf300300300100100100100100100100100100000111111hkyhkyhky280280280100100100100100100808080000121212wjjwjjwjj2702702701001001001…

2020牛客NOIP赛前集训营提高组(第四场)B-色球【链表】

正题 题目链接:https://ac.nowcoder.com/acm/contest/7611/B 题目大意 nnn个杯子&#xff0c;mmm个操作 在第zzz个杯子中依次加入xxx个颜色为yyy的球在第yyy个杯子中取出xxx个球&#xff0c;并询问最后一个拿出的球将第xxx个杯子的球依次拿出放入第yyy个杯子中 解题思路 每次…

POJ3613

POJ3613 题意&#xff1a;n条边构成的最短路 做法&#xff1a;倍增floyd #include <cstdio> #include <cstring> #include <algorithm> #define rep(i,a,b) for(int ia;i<b;i) typedef long long ll; const int N 207; const int inf 0x3f3f3f3f; using…

Ocelot-基于.NET Core的开源网关实现

写在前面API网关是系统内部服务暴露在外部的一个访问入口&#xff0c;类似于代理服务器&#xff0c;就像一个公司的门卫承担着寻址、限制进入、安全检查、位置引导等工作&#xff0c;我们可以形象的用下图来表示&#xff1a; 外部设备需要访问内部系统服务时必须要通过我们的AP…

【模拟】阿里郎(jzoj 2012)

阿里郎 jzoj 2012 题目大意&#xff1a; 有一个数n&#xff0c;求出它的所有约数d&#xff0c;有多少个约数就分多少种情况&#xff0c;每次分为d组&#xff0c;把1,d1,2d1……n-d1分为一组&#xff0c;以此类推&#xff0c;把2,d2,2d2……n-d2分为另一组&#xff0c;然后同…

[2020.10.25NOIP模拟赛]序列【Splay】

正题 题目链接:https://www.luogu.com.cn/problem/U136336?contestId36038 题目大意 第iii次找到第iii大的数字位置xix_ixi​并且翻转[i,xi][i,x_i][i,xi​]&#xff0c;求输出序列xxx 解题思路 记录一下每个排名在SplaySplaySplay中的位置&#xff0c;然后暴力翻转即可。 …

HDU2476

HDU2476 做法&#xff1a; 先想到用\(f[i]\)表示A的前i个字符变成B的最少涂得次数&#xff0c;不难写出方程&#xff0c;当\(A[i]≠B[i], f[i] max(f[j-1]cost[j][i])\), 当\(A[i]B[i]\)时&#xff0c;\(f[i]f[j-1]\) , \(cost[i][j]\) 表示将i到j涂成和B一样的最少的次数。现…

Blazor 0.5.0 升级及新特性介绍

前言喜大普奔&#xff0c; Blazor 0.5.0 在我刷了好几遍 Github &#xff08;表示功不可没&#xff09;以后&#xff0c;终于在 2018年7月25日发布了&#xff01;[Blazor 0.5.0 Announce] (https://blogs.msdn.microsoft.com/webdev/2018/07/25/blazor-0-5-0-experimental-rele…

【dfs】通行证(jzoj 2013)

通行证 jzoj 2013 题目大意&#xff1a; 有一个图&#xff0c;走某一条路需要某个通行证&#xff08;数据给出所有可选择的&#xff09;&#xff0c;现在要从某个点到另一个点&#xff0c;问最少要多少个通行证 样例输入 3 3 3 0 2 0 0 2 1 1 2 2样例输出 2 0 2数据范围限…

AT4378-[AGC027D]ModuloMatrix【构造】

正题 题目链接:https://atcoder.jp/contests/agc027/tasks/agc027_d 题目大意 n∗nn*nn∗n的网格&#xff0c;每个格子填一个数要求 所有数各不相同对于相邻的两个数ax,aya_x,a_yax​,ay​&#xff0c;max{ax,ay}%min{ax,ay}max\{a_x,a_y\}\%min\{a_x,a_y\}max{ax​,ay​}%mi…

HDU5693

HDU5693 做法&#xff1a; 同hdu2476也时两次dp的题&#xff0c;首先可以列出方程\(f[i] max(f[i-1], max(f[j-1](i-j1))*can[j][i])\)&#xff0c;\(can[j][i]\)表示从j到i能否完全消除。现在考虑如何求出\(can[l][r]\)&#xff0c;还是区间dp&#xff0c;任何一个长度≥2的等…

NuStore使用说明

简介.net core 部署有两种方式&#xff0c;一种是独立式部署&#xff08;SCD&#xff09;&#xff0c;另一种是框架依赖式部署&#xff08;FDD&#xff09;。以SCD方式生成发布包时&#xff0c;dotnet会将所有依赖打包到一个文件夹内&#xff0c;并为应用程序生成可执行文件。以…

【DP】【高精】逆序对(jzoj 2014)

逆序对 jzoj 2014 题目大意&#xff1a; 有一个长为n的序列&#xff08;由1,2,3,……n组成&#xff09;&#xff0c;问经过某种调整之后&#xff0c;有k个逆序对&#xff08;即在前面的一个数大于后面的一个数这样的对&#xff09;的种数&#xff0c;有多组数据&#xff0c;…

Codeforces274B

Codeforces274B 做法&#xff1a;树形dp&#xff0c;\(up[u]\)至少加多少次, \(down[u]\) 至少减多少次可以消去u这棵子树 转移&#xff1a;首先\(up[u] max(up[v]), down[u] max(down[u])\)&#xff0c;因为为了消除整颗子树一定要选最多的次数才能消完&#xff0c;那么现在…

HttpClientFactory与Steeltoe结合来完成服务发现

前言上一篇说了一下用HttpClientFactory实现了简单的熔断降级。这篇就来简单说说用HttpClientFactory来实现服务发现。由于标题已经好明显的说了Steeltoe因此这里会要求有Spring Clound的相关环境&#xff0c;本文也默认各位对这里有些许了解&#xff0c;所以不会涉及搭建过程的…

P4149-[IOI2011]Race【点分治】

正题 题目链接:https://www.luogu.com.cn/problem/P4149 题目大意 nnn个点的一棵树&#xff0c;求一条边最少的权值和为kkk的路径。 解题思路 直接点分治&#xff0c;然后用mapmapmap记录每个权值和的最小边数转移即可。 时间复杂度O(nlog⁡2n)O(n\log^2 n)O(nlog2n) codecod…