.NET Core TDD 前传: 编写易于测试的代码 -- 构建对象

该系列第1篇: 讲述了如何创造"缝".  "缝"(seam)是需要知道的概念.

本文是第2篇, 介绍的是如何避免在构建对象时写出不易测试的代码. 本文的概念性内容大部分都来自Misko Hevery的这篇博客文章.

构建

还是用上文里汽车的例子.

通常情况下, 我们是先去建造汽车, 组装好汽车后, 我们再去驾驶它.

软件开发也类似, 我们应该把对象构造完毕之后, 再去用它. 但是有时候, 开发者会在构造过程中添加一些程序逻辑. 这就相当于车还没造完, 我们就驾驶它去兜风了. 这样做是不太好的.

构造函数是类用来创建其实例对象的方法, 这里的代码是用来准备该对象的. 但有时开发者会在构造函数里做一些其它的工作, 例如构建依赖项, 执行初始化逻辑等等.

在构造函数(或者更大一点, 指构建的过程)里, 做这些额外的工作会让测试变得异常困难. 这是因为像初始化依赖项, 调用服务, 设置状态的逻辑等这些工作会把用于测试的"缝"弄丢. 导致无法进行mock.

总之在构造的过程中做太多的工作会妨碍测试.

 

危险信号

  • 在构造函数/字段声明里出现new关键字

    • 如果构造函数里需要创建依赖, 那么这就会为该类与依赖项之间创造了紧耦合. 这个之前提过, 所以需要注入依赖. 但是简单的值类型, 例如字符串, List, Dictionary等还是可以的.

  • 在构造函数/字段声明里调用静态方法

    • 静态方法不可以被mock, 也不能被注入.

  • 构造函数出现流程控制逻辑代码

    • 这样就很难对逻辑直接进行测试了. 我们只能分别使用不同的方式构造该对象, 测试并确认对象的状态. 而这个状态通常对直接测试是隐藏的. 实际上只要不是赋值代码, 就有可能是问题代码.

  • 构造函数里出现非赋值代码

  • 存在另外一个初始化函数 (也就是说构造函数走了完, 但是对象并没有被完全初始化)

 

如何解决问题?

  • 不要在构造函数里创建依赖项, 应该注入它们. 然后在构造函数里把它们赋值给类的私有变量.

  • 当需要构建对象图(一组有引用关系的对象), 也包括对象需要一些构建的参数等情况, 应该使用工厂, 建造者模式, 或者IoC容器的依赖注入等, 目的是把这些对象的构建工作分离出去.

  • 避免在构造函数里写逻辑代码, 例如条件, 循环, 计算等等. 也不能把逻辑代码放在别的方法, 然后调用该方法...

总之就是要避免对象的构建和对象的行为混合到一起, 因为它们在一起就会很难进行测试.

 

最后还有一点, 首先你需要知道, 根据angular的创始人Misko Hevery所说:

对象的构造分两类, 一种是可注入的, 一种是可new的.

可注入的对象可以由其它的一堆可注入对象组成. 它们可以为 可new的 对象工作. 可注入的对象通常是实现了接口的service, 像什么IUnitOfWork, IRepository, IxxxService等等.

可new的对象就是对象图里的终点, 例如实体或者值对象(Value Object)等.

为了易于测试, 针对这两类构造, 有下列规则:

可注入的对象可以在构造函数请求(注入)其它的可以注入对象, 但是不能在构造函数请求可new的对象.

反过来, 可new的对象可以在构造函数请求其它的可new对象, 但是不能在构造函数请求可注入的对象.

 

例子

第一个例子

640?wx_fmt=png

这是不对的, 构建的过程中直接new的话, 就会造成紧耦合, 也无法在测试中使用Test Double来代替它们了. 如果测试中不代替它们的话, 有些服务的开销可能会很大.

 

正确的写法是使用依赖注入:

640?wx_fmt=png

第二个例子

640?wx_fmt=png

该例中, UserController只需要UserService和LoggingService两个依赖项. 但是UserService又依赖于UserRepository. 

但是这样写就不对了, 这会造成UserController和UserRepository间的紧耦合, 而且配置UserService也并不是UserController的责任.

 

正确的写法是:

640?wx_fmt=png

而UserService也最好是注入依赖.

640?wx_fmt=png

 

而如果UserService并不是在构造函数注入UserRepository的话:

640?wx_fmt=png

那么Controller里就应该这样写:

640?wx_fmt=png

不过最好还是使用构造函数注入的写法.

 

第三个例子

640?wx_fmt=png

仔细的说, 该例有不止一处错误.

首先它有条件判断逻辑代码; 此外它还使用了ApplicationState.IsRunning这个静态变量(就是全局状态); 而且在构造函数里还做了UserService的配置工作, 这不是UserController的责任.

尽量要避免全局变量, 它无法进行隔离, 测试会遇到麻烦, 例如并行测试时其中一个测试改变了静态变量的值就可能导致另一个测试失败.

但是粗略的说, 该例可以说就是一个错误, 如何配置UserService并不是UserController的责任, 所以, 正确的做法是把UserService配置相关的代码移出去, 让它自己去管理吧:

640?wx_fmt=png

 

第四个例子

640?wx_fmt=png

该例子中, LoggingService的Log方法需要一个Area类型的对象, 它是一个值对象.

所以它的错误就是, 不应该把可new的对象注入到可注入的对象里. 这么做的话, 测试就不好做隔离了.

 

正确的做法应该是, 作为方法的参数传递进来:

640?wx_fmt=png

第五个例子

如果出现类类似initalize()或类似意思的方法, 很有可能说明该对象的责任太多了.

640?wx_fmt=png

 

修改它很简单, 让各自的类负责自己的内容即可. 去掉initialize()方法即可.

 

例子就举这些, 并不全, 详细请看Angular作者的博文.

 

测试/运行时如何建立对象

上面例子里的UserController就是我们需要使用的对象, 在运行时, 代码可能是这样的:

640?wx_fmt=png

构建这个对象还是有点麻烦的, 它的类关系图如下:

640?wx_fmt=png

 

所以测试的设置过程也会比较麻烦:

640?wx_fmt=png

当然也可以不直接new, 而是使用mock. 总之都很麻烦.

 

使用工厂

所以我们可以使用Factory等模式, 把构建UserController的工作放到工厂里:

640?wx_fmt=png

 

可以这样调用:

640?wx_fmt=png

 

使用IoC容器

如果项目使用了IoC容器的话, 还可以使用类似下面的用法:

640?wx_fmt=png 

先介绍到这里.

原文地址:http://www.cnblogs.com/cgzl/p/9375655.html

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

640?wx_fmt=jpeg

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

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

相关文章

Gym100187B

Gym100187B \(n\):表示字符的种类;\(a_i\):表示单词i的个数;\(Len\):表示串的长度; 先考虑一种单词\(a_i\)放在一个固定位置p对答案的贡献: 整一个串的全排类为:\[P \frac{Len!}{a_1!a_2!...a_…

【线段树】开关(luogu 3870)

开关 luogu 3870 题目大意: 有n个灯,每一次会按一个区间中的所有灯(开变关,关变开,操作0),或询问某个区间中有多少个灯是亮着的(操作2),按操作进行&#x…

HDU4455

HDU4455 做法:照着学姐的ppt做的。\(f[i]\)表示长度为i时的答案,考虑如何通过\(f[i-1]\)递推出\(f[i]\) eg:\(f[3] \rightarrow f[4]\) (1 1 2) 3 4 4 5 1 1 2 \(\rightarrow\) 1 1 2 3 :1 1 2 3 \(\rightarrow\) 1 2 3 4 :1 2 3 4 \(\rightarrow\) 2 3 …

YbtOJ#20072-[NOIP2020模拟赛B组Day6]相似子串【根号分治】

正题 题目链接:http://noip.ybtoj.com.cn/contest/105/problem/2 题目大意 一个010101串,qqq个询问,每次询问有多少个长度为mmm的子串010101个数与给出的010101串TTT相同 解题思路 因为询问串的总长与nnn同级,所以考虑根号分治 将询问的TT…

构建可扩展的有状态服务

原文链接:http://highscalability.com/blog/2015/10/12/making-the-case-for-building-scalable-stateful-services-in-t.html在很长一段时间内,分布式系统都采用无状态服务作为分布式系统扩展的最佳实践。它可以通过简单的循环负载均衡来提供扩展能力。…

51nod1743-雪之国度【最小生成树,LCA,并查集】

正题 题目链接:http://www.51nod.com/Challenge/Problem.html#problemId1743 题目大意 nnn个点mmm条边的一张图,每次询问要求找出x,yx,yx,y直接的两条不重路径的最大值最小。 解题思路 首先第一条路径肯定是最小生成树上的路径,所以我们先求出最小生成…

树莓派3B+,我要跑.NET CORE

前面的话我也不知道为什么,看到.net core觉得很爽,可以真正的跨平台,个人觉得很喜欢,所以就准备拿树莓派来验证我的想法。在我写这篇文章的时候,刚好在树莓派上装好了core2.1,先上结果。一、基础准备从板子…

Codeforces1019C

Codeforces1019C 题意:一张无自环的有向图,请找出一个点集Q,满足Q内的点不能互相有边,且Q内的点一定可以通过1次或2次移动,到达Q集合以外的任意一个点。 做法:思路神奇。做法就是编号小到大枚举选的点&…

WZK的排名(jzoj 1995)

WZK的排名 jzoj 1995 题目大意: 有一个成绩排名,让你按成绩排序(成绩相同,年级更小的在前面),让你求对于每个人来说,成绩比他大年级比他小的人的个数 输入样例 5 300 5 200 6 350 4 400 6 …

YbtOJ#20073-[NOIP2020模拟赛B组Day6]钻石守卫【构造】

正题 题目链接:http://noip.ybtoj.com.cn/contest/105/problem/3 题目大意 nnn个点mmm条边的图,保证每条边两边的点权和大于等于边权。现在要去减去最少/多的点权使得每条边的边权等于两边的点权。 解题思路 对与一个连通块,显然确定一个就可以确定别的…

【bfs】WZK旅游(jzoj 1996)

WZK旅游 jzoj 1996 题目大意: 给出一个nm的矩阵,表示一个地方的高度,高度相同且相邻(不算斜角)的算一块,当整一块高度都大于周围时,这是个山峰,相反当高度都低于附近时是湖泊&…

FWT学习笔记

FWT学习笔记 参考: 快速沃尔什变换(FWT)学习笔记FWT 详解 知识点定义: 快速沃尔什变换(FWT)主要解决位运算卷积的问题。给定两个数组 \(A\) 和 \(B\) (长度为2的整数幂):\[C_k \sum_{i \oplus jk}A_iB_i\] 其中\(\oplus\)可以是与&#xff0…

hdu5709-Claris Loves Painting【线段树合并】

正题 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid5709 题目大意 nnn个点的一棵树,每次有询问(u,k)(u,k)(u,k)表在uuu的子树中,距离uuu不超过kkk的节点中有多少不同颜色的节点。 解题思路 线段树维护每个深度有多少是颜色出现的最浅的位置&am…

【DP】【高精】WZK打雪仗(jzoj 1997)

WZK打雪仗 jzoj 1997 题目大意&#xff1a; 在一个环上有n*2个点&#xff0c;问有多少种连法可以用n条线连接成n对点 输入样例 5输出样例 42解释&#xff1a; 一种可行的方案如下&#xff1a; 数据范围 对于30%数据&#xff1a; n<30。 对于100%数据&#xff1a; …

月旦评 之 DevOps招贤令2018

公元164-182年间&#xff0c;汝南平舆的许氏兄弟于每月初一品评人物&#xff0c;褒贬时政&#xff0c;被称为“月旦评”。所谓“子治世之能臣&#xff0c;乱世之奸雄也”这句许邵评价曹操的话也是来自于“月旦评”&#xff1b;时间一下子来到了2018年&#xff0c;LEANSOFT DevO…

牛客网暑期ACM多校训练营(第九场)

牛客网暑期ACM多校训练营&#xff08;第九场&#xff09; A. Circulant Matrix 做法&#xff1a;看到下标 \(xor\) 这种情况就想 \(FWT\)&#xff0c;可是半天没思路&#xff0c;于是放弃了。。其实这个 \(n\) 疯狂暗示啊。设未知数向量为 \(x\)&#xff0c;列一下方程组就可以…

函数式编程之-模式匹配(Pattern matching)

编者&#xff1a;C# 7.0也加入了模式匹配&#xff0c;来源于F#。模式匹配在F#是非常普遍的&#xff0c;用来对某个值进行分支匹配或流程控制。模式匹配的基本用法模式匹配通过match...with表达式来完成&#xff0c;一个完整的模式表达式长下面的样子&#xff1a;match [somethi…

P4123-[CQOI2016]不同的最小割【网络流,分治】

正题 题目大意:https://www.luogu.com.cn/problem/P4123 题目大意 一张无向图&#xff0c;求所有点对之间有多少不同的最小割。 解题思路 考虑分治的做法&#xff0c;如果我们得知了(s,t)(s,t)(s,t)的最小割www&#xff0c;并且剩下的残量网络中连通点集SSS与sss连通&#x…

【DP】饥饿的WZK(jzoj 1998)

饥饿的WZK jzoj 1988 题目大意&#xff1a; 有很多个点&#xff0c;并且给出n个区间&#xff0c;问在选的区间不重复的前提下&#xff0c;选的区间的点数总和最大是多少 输入样例 3 1 3 7 8 3 4输出样例 5数据范围 对于100%的数据&#xff1a;1<N<2000&#xff0c…

ACM-ICPC 2018 徐州赛区网络预赛 D. EasyMath

ACM-ICPC 2018 徐州赛区网络预赛 D. EasyMath 做法&#xff1a;\[f(m,n) \sum _{i1}^{m} \mu(in) \sum_{i1}^{m}[gcd(i,n)1]\mu(i)\mu(n) \mu(n)\sum_{d|n}\mu(d)f(\frac{m}{d},d)\] 边界: n1&#xff0c;杜教筛求\(\sum_{i1}^{m}\mu(i)\)&#xff0c;m 1, 返回\(\mu(n)\)&…