从测试的角度看,理想情况下,我们的所作的全部测试都是对应了实际的代码,但这并不适用于实际情况,比如每次测试都去访问数据库,或者都去加载许多和待测代码毫无联系的配置文件,这些不但会增加测试的时间开销,同时也会增加测试用例的开发开销。并且这样也难以模拟一些特殊情况下的测试,比如需要特定的网络接入条件,或者当天是某个特殊日期等等。
这时候我们可以用一些模拟的代码来特换实际代码,从而帮助我们进行测试。前两天我司请来的老师来讲的:Stub和Mock,就是两种这样的模拟代码。
但是Stub和Mock的用法由于相似,都是为了替换外部依赖对象,从而经常被理解混淆,或者根本没有分清。但是实际上这是两种完全不同的事物:
- 首先它们对于怎么去确定测试结果使用的方式是不同的,其中一个使用行为去确认(behavior verification),一个使用状态去确认(state verification)
- 另一方面这是两种将测试和设计结合在一起的方法,一个是自顶向下,一个是自下而上
简言之,Stub更关注交互行为,为了验证待测系统调用目标系统接口的交互行为,而Mock更关注状态,为了验证待测系统调用了目标系统后,目标系统的状态。
public class OrderStateTester {
private WareHouse warehouse = new WareHouseImpl();
@Before
protected void setup() throws Exception {
warehouse.setSize(50);
warehouse.setLocation("Shang Hai");
}
@Test
public void testOrderIsFilledIfEnoughInWarehouse() {
Order order = new Order(50);
order.fill(warehouse);
assertTrue(order.isFilled());
assertEquals(0, warehouse.getInventory());
}
@Test
public void testOrderDoseNotRemoveIfNotEnough() {
Order order = new Order(51);
order.fill(warehouse);
assertFalse(order.isFilled());
assertEquals(50, warehouse.getInventory());
}
}
类似这样的TestCase是最常见的一种,可以看到我们需要测试的是Order对象。为了这个测试,需要Order跟Warehouse,需要Warehouse的理由有两个:首先需要通过它来配合测试,其次需要它来进行验证(因为order.fill改变了warehouse对象)
如果使用Mock对象会怎么样呢?有很多可用的mock对象库,Mokito,JMock之类的,如果用jMock写一段测试用例则会是:
public void testFillingRemovesInventoryIfInStock() {
Order order = new Order(50);
Mock warehouseMock = new Mock(Warehouse.class);
warehouseMock.expects(once()).method("hasInventory")
.with(eq(content),eq(50))
.will(returnValue(true));
warehouseMock.expects(once()).method("remove")
.with(eq(content), eq(50))
.after("hasInventory");
order.fill((Warehouse) warehouseMock.proxy());
warehouseMock.verify();
assertTrue(order.isFilled());
}
可以看到在数据准备的阶段,创建了一个Warehouse类的mock对象,接着设置了Mock的期望,这些期望也就是在测试order时会被执行。
在验证阶段,和之前一样可以跑order对象的断言,其次可以调用mock的verify方法,验证它是否像期望的那样去运行。
关键不同点在于怎么样去验证order在于warehouse的交互中做了正确的事。上面的testcase中,我通过warehouse的状态去验证。
如果对于Stub和Mock还是分不清楚的话,或许可以通过老师举得MailSender的case来解释一番:如果我们有一个发送邮件的服务,会和待测对象交互:
public interface MailSender {
public void send (Message msg);
}
如果使用Stub去验证:
public class MailSenderStub implements MailSender {
private List<Message> messages = new ArrayList<Message>();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
@Test
public void testOrder {
Order order = new Order(51);
MailSenderStub mailer = new MailSenderStub();
order.setMailer(mailer);
assertEquals(1 , mailer.numberSent());
}
我们不去关心它是否会发送给正确的人,或者发送的内容是否正确。
如果使用Mock去验证:
@Test
public void testOrderSendsMailIfUnFilled() {
Order order = new Order(51);
Mock mailer = mock(MailSender.class);
Mock warehouse = mock(Warehouse.class);
order.setMailer((MailSender)mailer.proxy());
warehouse.expects(once()).method("hasInventory").withAnyArgument()
.will(returnValue(false));
order.fill((Warehouse)warehouse.proxy())
}
两种方法都用了别的代码替代真正的MailSender,不同的是Stub采用行为验证,只要发送了邮件即可,而Mock采用了状态验证。
在重新学习了Stub和Mock之后,之前逐渐混淆的概念又有了新的理解,对于目前维护的系统中测试困难的问题,比如测试一段方法需要许多logger对象,或者需要查询DB,发现可以通过完善Stub来解决,而且由于目前系统的开发背景,logger等无关对象(与代码逻辑无关)的实现在多个项目中都几乎相同,所以可以用一套统一的Stub来实现多个系统的测试。而对于那些我们关心它状态的依赖,例如MessageSender,则可以通过Mock的方式实现并验证。
现在的老项目流传下来的祖传test case几乎没有一个能跑通的,下一步的目标就是保证新代码的测试覆盖率,以及在力所能及的范围里面把老代码的测试也搞起来 : )