深入浅出裸测之道---单元测试的单元化
三层架构之解耦和单元测试
依赖注入DI很大程度的帮助测试单元化。这对层与层之间的依赖关系,几乎是真理。
如对数据读写的依赖关系,用IRepository替换之后,所有用到IRepository的类,如Serivce这一层的ExamService,在测试时,只需要传入一个Mock的IRepository类,就不需要使用真实的数据库对它测试了.
我们的另外一层Controller也用到Service这一层,同样我为Service这一层的实现也提出一个接口IExamService,在Controller的构造器中传入IExamService的Mock类。因此,很容易的让测试关注于Controller本身的行为和功能。甚至可以在ExamService类实现之前,我们就可以测试和实现Controller类。这是依赖注入的优势。
这一整套分层,解耦和测试我们已经实现了,并形成一个规范的过程和成形的框架。现在已经简单到按部就班,就能轻松完成,甚至后期都可以考虑自动生成这部分代码。但这部分现在不是本文的重点。
业务域的简单案例---构造器赋值
当我们的注意力转移到业务域时,情景有了悄悄的改变。业务域中,类与类之间有更多更复杂的依赖关系。相比之下,三层之间反而简单。
这里,把我正在做的考试(Exam)类做一个简单的背景介绍。考试,对于身经百战的我们应该不陌生了,让我们好好分析,看看熟悉身影的陌生之面。另外,我这里考试更多是拿社会化考试作分析目标。
一个考试有三个很重要的要素:考试代码(考试定义);考区(北京考区,湖南考区);考试日期。这三个要素,唯一标识一个考试,也就是说,同一个考区,同一个考试定义在同日期,我就认为是同一个考试。很简单的逻辑,为了体现这个逻辑,我把这三个要素,放在考试类的构造器中。为什么?任何一个要素的缺失,考试对象的存在都没有任何含义,所以一开始构造的时候,就要传入。从另一个角度,考区+考试定义+日期是考试的业务ID,是唯一标识,必须贯穿于业务对象的始终。
看代码:
public class Exam { public Exam(District district, ExamDef exam_def, Date date) { District = district; ExamDef = exam_def; Date = date; } } |
通过构造器,从外部传入三个对象后,把它们赋给考试的三不属性,而这三个属性是只读,Private是为了给nHibernate和构造器使用的。为什么?如前所说他们是业务动,在创建之后,再修改没有任何含义。
看代码:
public class Exam { public Exam(District district, ExamDef exam_def, Date date) { District = district; ExamDef = exam_def; Date = date; } public virtual ExamDef ExamDef { get; private set; } public virtual District District { get; private set; } public virtual Date Date { get;private set; } } |
传统nUnit测试示例
好了,背景已经足够了。让我们来针对这部分功能进行测试。喂,等等,我们……现在有功能吗?有!我测试的描述就是,
当从构造器链构造考试类时,三个属性应该要赋相应的值。
是的,足够简单使我们一目了然,也足够复杂,我们需要用测试来保障它的功能。1、保证它被运行---覆盖测试;2、保证它是按我的设计进行的---行为测试。
看代码:
[TestFixture] public class when_create_an_exam { [Test] public void it_should_assign_parameters_to_properties() { //Arrange var stub_exam_def = new ExamDef("98"); var stub_district = new District("01"); var stub_date = new Date(2011, 1, 1); //Action var subject = new Exam(stub_district, stub_exam_def, stub_date); //Assert Assert.AreEqual(stub_district,subject.District); Assert.AreEqual(stub_exam_def,subject.ExamDef); Assert.AreEqual(stub_date,subject.Date); } } |
引入三个中间变量和另外三个类的定义我就不在这罗嗦了。我的命名方式也曾为人病诟,也不在这辩解。只看实质内容:分别创建三个类的实例,用于测试,至于这三个类的具体内容,我其实并不关心。所以用个词Stub来表示我的不关心。DDD的核心理念之一:名符其实。最后,我的断言只判断属性的值是否与构造器传入值相符。OK,完成!
坏味道?---重构的提出
过一段时,间。我们再回头看看这段测试,会有些小小的不舒服。特别,我们还有更多的类有类似的构造器赋值功能,还有更多更复杂的功能等着我们去测试,我们在做商业软件,不是吗?随着类似的测试更得越多。这些小小的不舒服会越积越大。
这面的测试有什么问题?
1、测试有三部分:建立测试环境;调用被测功能,(测试的本体);断言。上面的代码,我甚至都已经刻意用注释分离出了这么三块,但仍不是语法级别的分离。
2、对第三方的类依赖较为严重,这是本文的重点---单元测试单元化。对Exam类来说ExamDef, District都是插足的第三者。
3、测试代码太多,被测的实际上只有三行,虽然这不是原则性的问题,但是本着更好,更快,更强的精神,这个问题也是值得解决的。
好了,你提出的问题已经太多了,我没办法一下子解决。3个还多?是的,我们的口号是“只要一个好”。
MSpec的引入--- AAA语法
言归正传,让我们本着选代和重构的原则来把这些问题一个一个解决。是的,测试也需要重构,测试代码还有bug呢?一点不奇怪。你没碰到过?噢,因为你根本不写测试代码。
关于测试的三段式,我曾经看过有人确实在nUnit的框架下一步一步重构,形成良好了测试框架。这里我就不这么麻烦了,直接上工具MSpec!测试的三段式,有个说法,叫AAA语法,分别是Arrange,Action,Assert。3A级语法,多酷!
而MSpec用了自己的名词,分别是Establish, Because, It。看看下面改造之后的测试代码就清楚什么意思了。
看代码:
public class When_create_an_exam_by { private Establish context = () => { stub_exam_def = new ExamDef("98"); stub_district = new District("01"); stub_date = new Date(2011, 1, 1); }; private Because of = () => subject = new Exam(stub_district, stub_exam_def, stub_date); private It should_assign_to_properties = () => { subject.District.ShouldEqual(stub_district); subject.ExamDef.ShouldEqual(stub_exam_def); subject.Date.ShouldEqual(stub_date); }; private static ExamDef stub_exam_def; private static District stub_district; private static Date stub_date; private static Exam subject; } |
再看一看测试运行的结果,就明了代码即文档的含义了。
看截图:
从nUnit升级到MSpec,给人一种耳目一新的感觉。开始也许会有些不习惯。但是,一旦习惯之后再也不想回头了。
Rhino Mock --- 我演我
好了,看看第二个问题。一开始,我们依乎不觉得这是个大问题,不就是直接创建一个依赖美吗,创建就完了呗,一行代码而已。仍然,需要提醒注意,我们是在做商业软件。一旦展开了,一个类不可能只是一、两个类,特别是间接关联的,会更多,拔出萝卜带出泥。就拿这个考试类来说,在我们的实际项目中,它还有考试科目列表属性,还通过报考类与考生有间接联系。而报考类又与订单类,事务类有交互有关系。考虑所有这些级联关系,难道我为了测试这个构造赋值功能把所有的类全部创建出来?
再进一步思考,我们会给出一个自然的解决方案,把考区类,考试定义类抽象出两个接口来,构造器传入接口定义,而不是类本身。这其实是对层与层之间依赖注入的一个模仿。但是,相信我,这个方向是另一个梦魇的入口。业务域和多层之间完全是不同的环境。不想太深入讨论,可能独立一篇文章都打不住。
幸好,我们有另一个工具Rhino Mock,能帮助我们解决类的模拟的问题。改造之后的测试代码如下。唯一的影响是,你需要为被模拟的类,加入一个至少是protected的无参数构造器。这其实不是个大问题,如果你同时在项目中使用nHibernate的话,也会有类似的要求。
看代码:
public class When_create_an_exam { private Establish context = () => { stub_exam_def = MockRepository.GenerateMock<ExamDef>(); stub_district = MockRepository.GenerateMock<District>(); stub_date = MockRepository.GenerateMock<Date>(); }; //...此处省略的没有修改的代码 } |
可以看到,这一次的重构,把考试代码、考区代码等,其实你根本不关心的信息已经省略掉了。
AutoMocking --- 懒的最高境界
到这还不够,最后一个问题是填饱我们肚子的最有一块烧饼。
隆重介绍AutoMocking,自动模拟。当你的测试类从AutoMock的Specification类继承时,它会自动为你创建一个被测试对象subject,并且根据被测试对象构建器的参数定义,全自动的创建模拟对象。而引用这些模拟对象的方式,
很简单Dependency<ExamDef>,就是依赖注入的依赖这个词。已经不需要太多的解释---名如其实。
再看代码:
public class When_create_an_exam:Specification<Exam> { private It should_assign_to_properties = () => { subject.District.ShouldEqual(DependencyOf<District>()); subject.ExamDef.ShouldEqual(DependencyOf<ExamDef>()); subject.Date.ShouldEqual(DependencyOf<Date>()); }; } |
三行实现代码,对应三行测试代码。简洁的不能再简洁了。
最新内容请见作者的GitHub页:http://qaseven.github.io/
深入浅出裸测之道---单元测试的单元化相关推荐
- php 单元测试分享,今日分享:代码整洁之道- 单元测试
从开发的角度来讲,先把变量.函数按照一定的命名.格式组织好,接下来,开始编写代码,在业界,很多提倡测试驱动开发,接下和大家聊一下单元测试. TDD是测试驱动开发(Test-Driven Develop ...
- 测开- Junit 单元测试框架
文章目录 前言 了解 Junit 准备工作 - 在 pom.xml 文件中引入 Junit 相关依赖 1.Junit注解 @Test @BeforeEach.@BeforeAll @AfterEach ...
- Golang 单元测试详尽指引
文末有彩蛋. 作者:yukkizhang,腾讯 CSIG 专项技术测试工程师 本篇文章站在测试的角度,旨在给行业平台乃至其他团队的开发同学,进行一定程度的单元测试指引,让其能够快速的明确单元测试的方式 ...
- 芝麻翻译软件测试,细数35个单元测试准则 “Hello, world!”
1.保持单元测试小巧.快速 理论上,任何代码提交前都应该完整跑一遍全部测试套件.保持测试代码执行符合预期,这样可以缩短迭代开发周期.程序员 2.单元测试应该是全自动/非交互式的 测试套件一般是按期执行 ...
- 顶级业务架构设计的“道”与“术”,醍醐灌顶!
与智者为伍,足以睥睨天下. 架构的本质就是对系统进行有序化地重构,以满足当前业务的发展需求,同时能够实现快速扩展.而顶级业务架构师交付的解决方案既能拯救当下,提质增效,还能前瞻未来,护航发展. 向顶级 ...
- 软件测试集成测试ppt,软件测试单元测试和集成测试.ppt
软件测试单元测试和集成测试.ppt (31页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 19.90 积分 *内容(1)单元测试驱动程序桩程序互动(2)集 ...
- Golang单元测试指引
一.单元测试 1. 单元测试是什么 单元是应用的最小可测试部件.在过程化编程中,一个单元就是单个程序.函数.过程等:对于面向对象编程,最小单元就是方法,包括基类.超类.抽象类等中的方法.单元测试就是软 ...
- Java单元测试典型案例集锦
前言 近期,阿里巴巴CTO线卓越工程小组举办了阿里巴巴第一届单元测试比赛<这!就是单测>并取得了圆满成功.本人有幸作为评委,在仔细地阅读了各个小组的单元测试用例后,发现了两大单元测试问题: ...
- Android 单元测试,从小白到入门开始
目录 1 引言 1.1 背景 1.2 术语和缩略语 2 闲谈单测 2.1 说说我理解的单测 2.1.1 对测试金字塔的理解 2.1.2 为什么要做单测? 2.1.3 需要写 UI 测试吗? 2.1. ...
最新文章
- Debian/Ubuntu 报错解决:Command 'ifconfig' not found, but can be installed with
- NAT的全然分析及其UDP穿透的全然解决方式
- Debian部署postgresql并允许远程连接
- JavaFX学习之道:JavaFX之TableView
- JZOJ 5703. 【gdoi2018 day2】木板(board)
- python替换txt指定内容_python 实现类似sed命令的文件内容替换
- css隐藏输入框的光标
- C#操作ini文件类
- Fragstats 4.2 批处理(geotiff格式)
- Oracle 获取汉字拼音首字母
- QT使用ODBC连接MySQL
- QTableWidget 数据添加与表头设置
- xp系统禁止开机启动服务器,Window XP 开机启动超慢,哪些系统服务和进程可以禁用?...
- 写给女儿的话---小荷作文万米写书序言
- Seventh5: YAML syntax Ansible Playbook Ansible variables summaries and QQS | Cloud computing
- 工作这两年的经验与教训
- git add . 报错‘xxx/’does not have a commit checked out,fatal: adding files failed
- java 进阶笔记线程与并发之ForkJoinPool简析
- 信贷风险指标你都懂吗?
- 概率论与数理统计_数理统计部分