用模仿对象替换合作者以改进单元测试

发表于:2008-07-30来源:作者:点击数: 标签:单元合作者模仿对象改进
模仿对象(Mock object)是为起中介者作用的对象编写单元测试的有用方法。测试对象调用模仿域对象(它只断言以正确的次序用期望的参数调用了正确的方法),而不是调用 实际 域对象。然而,当测试对象必须创建域对象时,我们面临一个问题。测试对象如何知道创
模仿对象(Mock object)是为起中介者作用的对象编写单元测试的有用方法。测试对象调用模仿域对象(它只断言以正确的次序用期望的参数调用了正确的方法),而不是调用 实际域对象。然而,当测试对象必须创建域对象时,我们面临一个问题。测试对象如何知道创建 模仿域对象,而不是创建 实际域对象呢?在本文中,软件顾问 Alexander Day Chaffee 和 William Pietri 将演示一种重构技术,该技术根据工厂方法设计模式来创建模仿对象。

单元测试已作为软件开发的“最佳实践”被普遍接受。当编写对象时,还必须提供一个自动化测试类, 该类包含测试该对象性能的方法、用各种参数调用其各种公用(public)方法并确保返回值是正确的。

当您正在处理简单数据或服务对象时,编写单元测试很简单。 然而,许多对象依赖基础结构的其它对象或层。当开始测试这些对象时,实例化这些合作者(collaborator)通常是昂贵的、不切实际的或效率低的。

例如,要单元测试一个使用数据库的对象,安装、配置和发送本地数据库副本、运行测试然后再卸装本地数据库可能很麻烦。 模仿对象提供了解决这一困难的方法。模仿对象符合实际对象的接口,但只要有足够的代码来“欺骗”测试对象并跟踪其行为。 例如,虽然某一特定单元测试的数据库连接始终返回相同的硬连接结果,但可能会记录查询。 只要正在被测试的类的行为如所期望的那样,它将不会注意到差异, 而单元测试会检查是否发出了正确的查询。

夹在中间的模仿

使用模仿对象进行测试的常用编码样式是:

  • 创建模仿对象的实例
  • 设置模仿对象中的状态和期望值
  • 将模仿对象作为参数来调用域代码
  • 验证模仿对象中的一致性

虽然这种模式对于许多情况都非常有效,但模仿对象有时不能被传递到正在测试的对象。 而设计该对象是为了创建、查找或获得其合作者。

例如,测试对象可能需要获得对 Enterprise JavaBean(EJB)组件或远程对象的引用。或者,测试对象会使用具有副作用的对象,如删除文件的 File 对象,而在单元测试中不希望有这些副作用。

根据常识,我们知道这种情形下可以尝试重构对象,使之更便于测试。 例如,可以更改方法签名,以便传入合作者对象。

在 Nicholas Lesiecki 的文章“ Test flexibly with AspectJ and mock objects”中, 他指出重构不一定总是合意的,也不一定总是产生更清晰或更容易理解的代码。 在许多情况下,更改方法签名以使合作者成为参数将会在方法的原始调用者内部产生混淆的、未经试验的代码混乱。

问题的关键是该对象“在里面”获得这些对象。任何解决方案都必须应用于这个创建代码的所有出现。 为了解决这个问题,Lesiecki 使用了查找方式或创建方式。在这个解决方案中,执行查找的代码被返回模仿对象的代码自动替换。

因为 AspectJ 对于某些情况不是选项,所以我们在本文中提供了一个替代方法。 因为在根本上这是重构,所以我们将遵循 Martin Fowler 在他创新的书籍“ Refactoring: Improving the Design of Existing Code”(请参阅 参考资料)中建立的表达约定。 (我们的代码基于 JUnit ― Java 编程的最流行的单元测试框架,尽管它决不是 JUnit 特定的。)





回页首


重构:抽取和覆盖工厂方法

重构是一种代码更改,它使原始功能保持不变,但更改代码设计,使它变得更清晰、更有效且更易于测试。 本节将循序渐进地描述“抽取”和“覆盖”工厂方法重构。

问题:正在测试的对象创建了合作者对象。必须用模仿对象替换这个合作者。


重构之前的代码
class Application {
...
public void run() {
View v = new View();
v.display();
...

解决方案:将创建代码抽取到工厂方法,在测试子类中覆盖该工厂方法,然后使被覆盖的方法返回模仿对象。 最后,如果可以的话,添加需要原始对象的工厂方法的单元测试,以返回正确类型的对象:


重构之后的代码
class Application {
...
public void run() {
View v = createView();
v.display();
...
protected View createView() {
return new View();
}
...
}

该重构启用清单 1 中所示的单元测试代码


清单 1. 单元测试代码
class ApplicationTest extends TestCase {
MockView mockView = new MockView();
public void testApplication {
Application a = new Application() {
protected View createView() {
return mockView;
}
};
a.run();
mockView.validate();
}
private class MockView extends View
{
boolean isDisplayed = false;
public void display() {
isDisplayed = true;
}
public void validate() {
assertTrue(isDisplayed);
}
}
}

角色

该设计引入了由系统中的对象扮演的下列角色:

  • 目标对象:正在测试的对象
  • 合作者对象:由目标对象创建或获取的对象
  • 模仿对象:遵循模仿对象模式的合作者的子类(或实现)
  • 特殊化对象:覆盖创建方法以返回模仿对象而不是合作者的目标的子类

技巧

原文转自:http://www.ltesting.net