[转载]模拟对象(mocknbsp;object)是如何出现的

模拟对象(mocknbsp;object)是如何出现的
作者:SteveFreeman  steve@m3p.co.uk 
翻译:胡拥军 hu.yong.jun@ctgpc.com.cn

1、介绍 1
2、简单的单元测试 1
3、分解出一个马达来 3
4、这意味着什么? 6
5、结论 8

1、介绍

模拟对象是一项开发技术,它让你对那些你认为不可能测试的类进行单元测试,帮助你写出更好的
代码。本文用一个简单的例子说明模拟对象的发现过程,这个例子将传统的单元测试一步一步重构成使
用模拟对象的单元测试。

模拟对象基于2个关键概念:除了要测试的代码,其它的一切都用模拟实现取代,模拟实现仿真了
剩余的环境;在模拟实现中放置测试断言,以便你能验证测试用例中对象间的交互。

本文假设你熟悉Java语言和JUnit测试框架。

关键词:模拟对象(mock object),单元测试(unit test)

2、简单的单元测试

我们以一个例子开始。大多数人开始写测试时是先写一个对象,然后调用这个对象的一些方法,最
后检查对象的状态。例如,假设我写一个构件,它自动地指导一个机器人穿过一个仓库。在网格中给定
一个起始点,机器人会找到一条路线到达指定的终点。那么测试应当告诉我们,机器人到达正确的终点,
一路上没有碰倒任何东西。第一个测试会是这样:

public class TestRobot {
[...]
public void setUp() {
robot = new Robot();
}

public void testGotoSamePlace() {
final Position POSITION = new Position(1, 1);
robot.setCurrentPosition(POSITION);
robot.goTo(POSITION);

assertEquals("Should be same", POSITION, robot.getPosition());
}
}

这个测试告诉我们,机器人到达了终点,但无法告诉我们它如何到达终点的。我还想知道在这个测
试中,机器人其实并没有移动。一个可选的做法是存放和提取机器人的每一步goto()的路线。如下:

public void testGotoSamePlace() {
final Position POSITION = new Position(0, 0);
robot.setCurrentPosition(POSITION);
robot.goTo(POSITION);

assertEquals("Should be empty", 0, robot.getRecentMoveRequests().size());
assertEquals("Should be same", POSITION, robot.getPosition());
}

下一个测试是在网格中移动一步:

public void testMoveOnePoint() {
final Position DESTINATION = new Position(1, 0);
robot.setCurrentPosition(new Position(0, 0));
robot.goto(DESTINATION);

assertEquals("Should be destination", DESTINATION, robot.getPosition());
List moves = robot.getRecentMoveRequests();
assertEquals("Should be one move", 1, moves.size());
assertEquals("Should be same move", new MoveRequest(1, MoveRequest.SOUTH), moves.get(1));
}

随着测试变得复杂,我要把一些细节放进帮助方法中,使代码更清晰:

public void testMoveALongWay() {
final Position DESTINATION = new Position(34, 71);
robot.setCurrentPosition(new Position(0, 0));
robot.goto(DESTINATION);

assertEquals("Should be destination", DESTINATION, robot.getPosition());
assertEquals("Should be same moves", makeExpectedLongWayMoves(), robot.getRecentMoves());

在这里,makeExpectedLongWayMoves()方法返回一个移动步列表,在这个测试中,我们希望机器人
执行一系列移动步。

这种测试途径有问题。首先,象这样的测试在小规模集成测试中是有效的,它们设置前置条件,测
试后置条件,但它们无法对正在运行的代码进行访问。如果这些测试中有一个失败,由于在方法调用完
成后就进行断言判断,因此我就不得不一步一步检查代码发现问题所在。其次,为了测试这个行为,我
要在产品类中增加一些功能,即在最后goto()执行后,保留所有的MoveRequests,我不需要
getRecentMovesRequests()方法。第三,虽然此处没有这个要求,基于从对象中提取历史的测试套件需
要一些工具方法,如构造和比较收集值的方法。如果需要写外部代码来操纵一个对象,通常预示这个对
象的类是不完整的,一些行为要从外部方法中移到这个类里来。

有更好的方法吗?能找到给出更好的错误报告和将行为放进合适位置的实现形式吗?

3、分解出一个马达来

我能确定一件事就是,机器人包含有一个马达,它响应移动请求。如果我能截取这些请求,我就能
跟踪机器人中发生了什么。先定义一个马达接口:

public interface Motor {
void move(MoveRequest request);
}

传递一个马达实例给机器人,可以通过构造器的方式,一个简单的实现如下:

public class OneSpeedMotor implements Motor {
public void move(MoveRequest request) {
turnBy(request.getTurn());
advance(request.getDistance());
}
[...]
}

现在,我要重构测试,用模拟马达(MockMotor)取代真正的Motor,MockMotor能观察到机器人中
发生了什么,如果出现错误它就会抱怨。测试就象这样:

public void testGotoSamePlace() {
final Position POSITION = new Position(0, 0);
robot.setCurrentPosition(POSITION);

Motor mockMotor = new Motor() {
public void move(MoveRequest request) {
fail("There should be no moves in this test");
}
};
robot.setMotor(mockMotor);

robot.goTo(POSITION);

assertEquals("Should be same", POSITION, robot.getPosition());
}

在这个测试中,如果机器人代码中有一个臭虫,马达又收到移动请求,那么对move()方法的模拟实
现就会立即失败,并中止测试,我也不再需要知道机器人在哪个位置上。随着测试代码的不断增长,我
能在一个单个的、更复杂的MockMotor中重构不同的模拟实现。并在所有的机器人测试中使用它,例如:

public void MockMotor implements Motor {
private ArrayList expectedRequests = new ArrayList();
public void move(MoveRequest request) {
assert("Too many requests", this.expectedRequests.size() > 0);
assertEquals("Should be next request", this.expectedRequests.remove(0), request);
}
public void addExpectedRequest(MoveRequest request) {
this.expectedRequests.add(request);
}
public void verify() {
assertEquals("Too few requests", 0, this.expectedRequests.size());
}
}

那么我们的测试就象这样:

public class TestRobot {
[...]
static final Position ORIGIN = new Position(0, 0);
public void setUp() {
mockMotor = new MockMotor();
robot = new Robot(mockMotor);
robot.setCurrentPosition(ORIGIN);
}
public void testGotoSamePlace() {
robot.goTo(ORIGIN);

assertEquals("Should be same", ORIGIN, robot.getPosition());
mockMotor.verify();
}
public void testMoveOnePoint() {
final Position DESTINATION = new Position(1, 0);

mockMotor.addExpectedRequest(new MoveRequest(1, MoveRequest.SOUTH));

robot.goto(DESTINATION);

assertEquals("Should be destination", DESTINATION, robot.getPosition());
mockMotor.verify();
}
public void testMoveALongWay() {
final Position DESTINATION = new Position(34, 71);

mockMotor.addExpectedRequests(makeExpectedLongWayMoveRequests());

robot.goto(DESTINATION);

assertEquals("Should be destination", DESTINATION, robot.getPosition());
mockMotor.verify();
}
}

4、这意味着什么?

我们的代码朝着这样一个方向发展,我们要确定单元测试,但不想将机器人的不必要的细节状态暴
露出来(如getRecentMoveRequests()方法所作的那样)。结果,我发现得到了一个更好的单元测试,
通过增加一个马达接口,使得机器人的内部结构更清楚。现在,我们获得了在机器人中切换不同的马达
实现的灵活性,也许可能的一个马达实现是一个加速马达。类似地,如果我想跟踪机器人移动的总距离,
不用改变机器人或马达的实现就能达到这个要求:

/**
* A decorator that accumulates distances, then passes the request
* on to a real Motor.
*/
public class MotorTracker implements Motor {
private Motor realMotor;
private long totalDistance = 0;

public MotorTracker(Motor aRealMotor) {
realMotor = aRealMotor;
}
public void move(MoveRequest request) {
totalDistance += request.getDistance();
realMotor.move(request);
}
public long getTotalDistance() {
return totalDistance;
}
}

// When constructing the Robot, wrap the implementation of a
// Motor that does the work in a MotorTracker.
OneSpeedMotor realMotor = new OneSpeedMotor();

MotorTracker motorTracker = new MotorTracker(realMotor);

Robot = new Robot(motorTracker);

// do some travelling here [...]
println("Total distance was: " + motorTracker.getTotalDistance());

无需改变马达实现,也不必跟踪功能,就这么容易,只要我们使用这个测试策略,它基于一个隐藏
的中间机制。下一步,引进一个浏览对象来实现该机制,机器人会把这2者关联起来。

基于模拟对象的测试通常遵循一个模式:设置一些状态,预先设置对测试的期望值,运行目标代码,
然后检查结果是否与期望值一致。由于所有测试的结构都是类似的,且与对象的所有交互都局部于测试
的固定代码中,因此这种方式使测试变得容易。于是我发现这个测试结构可以贡献给其他人,使他们的
测试只需花几分钟。然而,更重要的,我经常发现,决定一个测试要检查什么这个过程驱使我阐清对象
与其协作者的关系。我在代码中增加了一个伸缩点,当代码进化时提供测试可以伸缩的支持。

5、结论

这个简单的例子展示了重构一个测试的过程,通过使用脑海里设计原则,导致对一个不寻常的开发
技术成果的发现。在实践中运用模拟对象会有一点点不同。首先,编写测试的过程包括定义哪些模拟对
象,而不是一边进展一边定义。其次,对于Java语言,通用的模拟对象库工作正在开始进行。最后,已
经有几个工具和库可以帮助你构建模拟对象,如http://www.mockobjects.com站点就有一个期望对象库。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-132306/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/374079/viewspace-132306/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值