该系列第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容器的话, 还可以使用类似下面的用法:

先介绍到这里.

.NET Core TDD 前传: 编写易于测试的代码 -- 构建对象的更多相关文章

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

    有时候不是我们不想做单元测试, 而是这代码写的实在是没法测试.... 举个例子, 如果一辆汽车在产出后没完成测试, 那么没人敢去驾驶它. 代码也是一样的, 如果项目未能进行该做的测试, 那么客户就不敢 ...

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

    转载于: https://www.cnblogs.com/cgzl/p/9365955.html 有时候不是我们不想做单元测试, 而是这代码写的实在是没法测试.... 举个例子, 如果一辆汽车在产出后 ...

  3. .NET Core TDD 前传: 编写易于测试的代码 -- 全局状态

    第1篇: 讲述了如何创造"缝".  "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 第3篇, 依赖项和迪米特法则. 本文是 ...

  4. .NET Core TDD 前传: 编写易于测试的代码 -- 单一职责

    第1篇: 讲述了如何创造"缝".  "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 第3篇, 依赖项和迪米特法则. 第4篇 ...

  5. .NET Core TDD 前传: 编写易于测试的代码 -- 依赖项

    第1篇: 讲述了如何创造"缝".  "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 本文是第3篇, 讲述依赖项和迪米特法则 ...

  6. 新书《编写可测试的JavaScript代码 》出版,感谢支持

    本书介绍 JavaScript专业开发人员必须具备的一个技能是能够编写可测试的代码.不管是创建新应用程序,还是重写遗留代码,本书都将向你展示如何为客户端和服务器编写和维护可测试的JavaScript代 ...

  7. 编写可测试的JavaScript代码

    <编写可测试的JavaScript代码>基本信息作者: [美] Mark Ethan Trostler 托斯勒 著 译者: 徐涛出版社:人民邮电出版社ISBN:9787115373373上 ...

  8. 从一张图开始,谈一谈.NET Core和前后端技术的演进之路

    从一张图开始,谈一谈.NET Core和前后端技术的演进之路 邹溪源,李文强,来自长沙.NET技术社区 一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和 ...

  9. 编写Avocado测试

    编写Avocado测试 现在我们开始使用python编写Avocado测试,测试继承于avocado.Test. 基本例子 创建一个时间测试,sleeptest,测试非常简单,只是sleep一会: i ...

随机推荐

  1. 理解主从设备模式(Master-Slave)

    前言 在给定上下文的软件体系结构中,为了解决某些经常出现的问题而形成的通用且可重用的解决方案称之为架构模式,而常见的体系架构模式主要有以下十种 分层模式 客户端-服务器模式 主从设备模式 管道-过滤器 ...

  2. scrapy分布式爬虫scrapy_redis一篇

    分布式爬虫原理 首先我们来看一下scrapy的单机架构:     可以看到,scrapy单机模式,通过一个scrapy引擎通过一个调度器,将Requests队列中的request请求发给下载器,进行页 ...

  3. Asp.Net Core NLog 将日志输出到数据库以及添加LayoutRenderer的支持

    在这之前打算用Apache的Log4Net,但是发现其AdoNetAppender方法已经不存在了,无法使用配置文件直接输出到数据库了,因此我便改用了NLog框架. 一.对项目添加NLog 通过Nug ...

  4. Java通过JDBC 进行MySQL数据库操作

    转自: http://blog.csdn.net/tobetheender/article/details/52772157 Java通过JDBC 进行MySQL数据库操作 原创 2016年10月10 ...

  5. lookup_peer.go

    , fmt.Sprintf("LOOKUP connecting to %s", lp.addr))     conn, err := net.DialTimeout(" ...

  6. 阅读nsq源码 ---初步架构设计图

     

  7. spot 状压dp

    题目大意:数轴上有n个泥点,共有m个木板,求最少用几个木板可以覆盖全部泥点,并求最优方案数(n,m<=15) 看范围,肯定是状压 f[i][j]表示前i个泥点都被覆盖,使用的木板集合为j 转移: ...

  8. BZOJ_1266_[AHOI2006]上学路线route_最小割

    BZOJ_1266_[AHOI2006]上学路线route_最小割 Description 可可和卡卡家住合肥市的东郊,每天上学他们都要转车多次才能到达市区西端的学校.直到有一天他们两人参加了学校的信 ...

  9. BZOJ_1260_[CQOI2007]涂色paint _区间DP

    BZOJ_1260_[CQOI2007]涂色paint _区间DP 题意: 假设你有一条长度为5的木版,初始时没有涂过任何颜色.你希望把它的5个单位长度分别涂上红.绿.蓝.绿.红色,用一个长度为5的字 ...

  10. cocoapods安装及使用其中 添加新源: gem sources -a https://ruby.taobao.org/

    一.概要 iOS开发时,项目中会引用许多第三方库,CocoaPods(https://github.com/CocoaPods/CocoaPods)可以用来方便的统一管理这些第三方库. 二.安装 由于 ...