求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
三层架构之解耦和单元测试
 
作者 高磊,火龙果软件    发布于 2014-04-29
 

依赖注入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>());
};
}

三行实现代码,对应三行测试代码。简洁的不能再简洁了。

相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践
 
分享到
 
 
     


LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...