很多人经常会混淆这两个测试用的术语,要想完全理解测试替身(test doubles)的用法,我们就必须搞清楚 mocks 和其他术语的区别。当我们进行测试的时候,通常一次只关注一个元素,所以产生了一个术语叫单元测试。问题在于为了使得某个独立单元工作,通常需要其他单元的配合,因此在这个情况下我们需要某种类型的替代品。

在上文(见原文)提到的两个测试类型中,前者使用的是一个真实的对象,而后者使用的是一个 mock 对象,也就是非真实的。使用 mock 是在测试中避免使用真实对象的方法之一,同时也有其他形式的模拟手段。描述这些手段的词汇就很繁杂了,比如 stub, mock, fake, dummy。在这篇文章中,我使用 Gerard Meszaros 的书中所定义的词汇,虽然不是每个人都认可他,但我比较赞同。

Meszaros 使用 Test Double 这个术语来描述任何一个在测试中用于代替真实对象的虚拟对象,这个名称源于电影中替身演员的概念。随后 Meszaros 定义了五种典型的虚拟对象:

  • Dummy 被传递但从不使用,通常只用于填充形式参数。
  • Fake 则具有可以正常工作的实现,但通常采用了一些不适合生产环境的便捷手段。(一个典型例子是内存数据库)。
  • Stub 在测试中总是返回固定的返回值,对于与测试无关的代码,通常直接忽略。
  • SpyStub 一样,但是它会记录自身被调用的情况。一个例子是邮件服务会记录发送了多少封邮件。
  • 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,区别在于它具体是怎样工作的。我把这个问题留给你自己探索。

Last modification:January 9, 2021