.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对象, 但是不能在构造函数请求可注入的对象.
例子
第一个例子
这是不对的, 构建的过程中直接new的话, 就会造成紧耦合, 也无法在测试中使用Test Double来代替它们了. 如果测试中不代替它们的话, 有些服务的开销可能会很大.
正确的写法是使用依赖注入:
第二个例子
该例中, UserController只需要UserService和LoggingService两个依赖项. 但是UserService又依赖于UserRepository.
但是这样写就不对了, 这会造成UserController和UserRepository间的紧耦合, 而且配置UserService也并不是UserController的责任.
正确的写法是:
而UserService也最好是注入依赖.
而如果UserService并不是在构造函数注入UserRepository的话:
那么Controller里就应该这样写:
不过最好还是使用构造函数注入的写法.
第三个例子
仔细的说, 该例有不止一处错误.
首先它有条件判断逻辑代码; 此外它还使用了ApplicationState.IsRunning这个静态变量(就是全局状态); 而且在构造函数里还做了UserService的配置工作, 这不是UserController的责任.
尽量要避免全局变量, 它无法进行隔离, 测试会遇到麻烦, 例如并行测试时其中一个测试改变了静态变量的值就可能导致另一个测试失败.
但是粗略的说, 该例可以说就是一个错误, 如何配置UserService并不是UserController的责任, 所以, 正确的做法是把UserService配置相关的代码移出去, 让它自己去管理吧:
第四个例子
该例子中, LoggingService的Log方法需要一个Area类型的对象, 它是一个值对象.
所以它的错误就是, 不应该把可new的对象注入到可注入的对象里. 这么做的话, 测试就不好做隔离了.
正确的做法应该是, 作为方法的参数传递进来:
第五个例子
如果出现类类似initalize()或类似意思的方法, 很有可能说明该对象的责任太多了.
修改它很简单, 让各自的类负责自己的内容即可. 去掉initialize()方法即可.
例子就举这些, 并不全, 详细请看Angular作者的博文.
测试/运行时如何建立对象
上面例子里的UserController就是我们需要使用的对象, 在运行时, 代码可能是这样的:
构建这个对象还是有点麻烦的, 它的类关系图如下:
所以测试的设置过程也会比较麻烦:
当然也可以不直接new, 而是使用mock. 总之都很麻烦.
使用工厂
所以我们可以使用Factory等模式, 把构建UserController的工作放到工厂里:
可以这样调用:
使用IoC容器
如果项目使用了IoC容器的话, 还可以使用类似下面的用法:
先介绍到这里.
原文地址:http://www.cnblogs.com/cgzl/p/9375655.html
.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com
.NET Core TDD 前传: 编写易于测试的代码 -- 构建对象相关推荐
- .NET Core TDD前传: 编写易于测试的代码 -- 缝
有时候不是我们不想做单元测试, 而是这代码写的实在是没法测试.... 举个例子, 如果一辆汽车在产出后没完成测试, 那么没人敢去驾驶它. 代码也是一样的, 如果项目未能进行该做的测试, 那么客户就不敢 ...
- .NET Core TDD 前传: 编写易于测试的代码 -- 全局状态
第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 第3篇, 依赖项和迪米特法则. 本文是 ...
- .NET Core TDD 前传: 编写易于测试的代码 -- 依赖项
第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 本文是第3篇, 讲述依赖项和迪米特法则 ...
- 单元测试:如何编写可测试的代码及其重要性
原文来自互联网,由长沙DotNET技术社区编译.如译文侵犯您的署名权或版权,请联系小编,小编将在24小时内删除.限于译者的能力有限,个别语句翻译略显生硬,还请见谅. 作者:谢尔盖·科洛迪(SERGEY ...
- 如何编写可测试的代码 哈利勒的方法论
Understanding how to write testable code is one of the biggest frustrations I had when I finished sc ...
- ORAN专题系列-18:5G O-RAN FrontHaul前传接口互操作性测试规范IOT概述与总体架构
前言: 前传接口(FrontHual)是传统的BBU与RU之间的接口,在O-RAN之前,前传接口虽然定义了物理连接的CPRI标准,但CPRI之上承载的M plane和S/C/U plane的IQ数据, ...
- 用java写穿越火线代码_编写可测试的 JavaSript 代码
无论我们使用和Node配合在一起的测试框架,例如Mocha或者Jasmine,还是在像PhantomJS这样的无头浏览器中运行依赖于DOM的测试,和以前相比,我们有更好的方式来对JavaScript进 ...
- ORAN专题系列-19:5G O-RAN FrontHaul前传接口M Plane互操作性测试IOT规范
前言: 在<ORAN专题系列-18:5G O-RAN FrontHaul前传接口互操作性测试规范IOT概述与总体架构>阐述了5G O-RAN FrontHaul前传接口互操作性测试规范IO ...
- 《编写可测试的JavaScript代码》——1.4 小结
本节书摘来自异步社区<编写可测试的JavaScript代码>一书中的第1章,第1.4节,作者: [美]Mark Ethan Trostler 译者: 徐涛 更多章节内容可以访问云栖社区&q ...
最新文章
- J2ME游戏开发中时钟的简单实现
- CSS Hack汇总(转载)
- 图文并茂重新认识下递归
- 【剑指offer】_19 滑动窗口中的最大值
- 计算机仿真在哪学,计算机仿真软件有哪些
- poj3190 区间贪心 挑战程序设计竞赛
- [转] Async/Await替代Promise的6个理由
- UI设计师——你是什么设计师?
- day2.数据类型的操作和方法
- Windows 8 自带定时关机的4种实现方法
- Docker中ubuntu镜像安装ps显示进程
- sql—labs通关
- Python学习-操作列表
- 免费午餐 20个梦幻级开源免费软件
- bitmap file res\xxx.png is not in 3.00 format
- MATLAB实现离散系统Z域分析
- 【创业日记1】智慧旅游大数据服务平台项目-开始
- 中国移动深圳分公司笔试题
- 多媒体信息设计原则有哪些
- C++——分数化小数
热门文章
- Unbuntu 自动重启MySQL
- Hadoop部署方式-高可用集群部署(High Availability)
- 3月6日云栖精选夜读:如何实现32.5万笔/秒的交易峰值?阿里交易系统TMF2.0技术揭秘...
- 思科三层交换机充当路由器实现全网互通
- html5做的太阳系
- 常用安卓开发技巧汇总
- 可能是.NET领域性能最好的对象映射框架——Mapster
- Log4j 2漏洞(CVE-2021-44228)的快速响应
- switch类型模式
- .NET6下周发布真的香,可不少人却只会.NET Framework!