[译] Mocks 与 Stubs 的区别
原始链接:https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs
很多人经常会混淆这两个测试用的术语,要想完全理解测试替身(test doubles)的用法,我们就必须搞清楚 mocks 和其他术语的区别。当我们进行测试的时候,通常一次只关注一个元素,所以产生了一个术语叫单元测试
。问题在于为了使得某个独立单元工作,通常需要其他单元的配合,因此在这个情况下我们需要某种类型的替代品。
在上文(见原文)提到的两个测试类型中,前者使用的是一个真实的对象,而后者使用的是一个 mock 对象,也就是非真实的。使用 mock 是在测试中避免使用真实对象的方法之一,同时也有其他形式的模拟手段。描述这些手段的词汇就很繁杂了,比如 stub
, mock
, fake
, dummy
。在这篇文章中,我使用 Gerard Meszaros 的书中所定义的词汇,虽然不是每个人都认可他,但我比较赞同。
Meszaros 使用 Test Double
这个术语来描述任何一个在测试中用于代替真实对象的虚拟对象,这个名称源于电影中替身演员的概念。随后 Meszaros 定义了五种典型的虚拟对象:
- Dummy 被传递但从不使用,通常只用于填充形式参数。
- Fake 则具有可以正常工作的实现,但通常采用了一些不适合生产环境的便捷手段。(一个典型例子是内存数据库)。
- Stub 在测试中总是返回固定的返回值,对于与测试无关的代码,通常直接忽略。
- Spy 和
Stub
一样,但是它会记录自身被调用的情况。一个例子是邮件服务会记录发送了多少封邮件。 - Mock 就是我们这里所谈论的:它根据预先编写的逻辑,基于调用者所期望的返回值。
在所有这些类型中,只有 mock 进行行为验证,而其他类型的虚拟对象通常只能进行状态验证。和其他类型一样,mock 在测试过程中也具有一些行为,以便让待测程序正常工作,但在设置和验证阶段有所不同。
为了进一步研究测试替身,我们需要扩展一下之前的例子。很多人只在真实的对象难以配合进行测试时才会使用测试替身。比如一个常见的用例:如果订单异常,那么就需要发送一封邮件通知。问题是在测试期间我们并不想真正给客户发一封邮件过去,所以我们为邮件系统创建一个我们可以控制操作的测试替身。
现在我们可以来研究 mock 和 stub 之间的区别了。假设我们正为发送邮件这一行为编写测试,代码大致如下:
public interface MailService {
public void send (Message msg);
}
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
接着就可以进行状态验证:
class OrderStateTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent());
}
这个测试非常简单,只是发了一个消息。尽管我们并没有测试它是否发给了正确的用户,或者邮件内容是否正确,但足够演示用了。
现在使用 mock 替身,代码差别就很大:
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
}
在这两个例子中,我都使用了测试替身而不是真实的邮件服务。但 stub 进行了状态验证,mock 却进行了行为验证。对于 stub 对象,我需要编写额外的代码来帮助进行状态验证。最终 stub 实现了 MailService
接口,但是添加了额外的方法。
mock 总是使用行为验证,stub 既可以进行行为验证,也可以用于状态验证。Meszaros 把进行行为验证的 stub 叫做 spy
,区别在于它具体是怎样工作的。我把这个问题留给你自己探索。