不要模拟( Mock )不属于你负责的组件!

乔梁 | 2021-04-18

本文将讨论如何以及在什么地方应该使用 Mock 技术来模拟第三方库或外部组件。

虽然对外部组件的模拟( Mocks )让你能够验证系统的边界,而不必真正使用外部系统。但是,它也创建了一个不自然且有可能偏离了真实代码的机会,引入测试风险。

下面的测试代码就使用了 Mock 技术来模拟第三方库。

这么做,可能会引发什么样的问题呢?

// Mock a salary payment library
@Mock SalaryProcessor mockSalaryProcessor;
@Mock TransactionStrategy mockTransactionStrategy;
...
when(mockSalaryProcessor.addStrategy()).thenReturn(mockTransactionStrategy);
when(mockSalaryProcessor.paySalary()).thenReturn(TransactionStrategy.SUCCESS);
MyPaymentService myPaymentService = new MyPaymentService(mockSalaryProcessor);
assertThat(myPaymentService.sendPayment()).isEqualTo(PaymentStatus.SUCCESS);

模拟你无权掌控的类型会让测试维护工作更困难:

  • 第三方库的升级变得更困难:通常在 Mock 中硬编码了某个 API 的预期返回值,而这个值有可能被我们写错了,也可能随时间而过时。当升级第三方库的新版本时,手动更新测试例也需要花大量时间。

在上面的示例中,当一个新版本修改了“ addStrategy() ”,使其可以返回一个从“TransactionStrategy”派生出来的新类(例如“ SalaryStrategy ”)。即便被测试代码根本不需要修改(因为它仍旧是引用 TransactionStrategy ),可为了自动化测试能够成功,你还是要去修改你的 Mock 。

  • 很难知道第三方库的更新是否在你的代码中引入了 bug:随着第三方库的变更,你在 Mock 中所内置的原定假设可能过时了,因此导致:即使在被测代码有 bug 的情况下,测试也能成功通过。

在上面的示例中,如果第三方库将“ payalary() ”改为返回 TransactionStrategy.SCHEDULED ,由于被测代码未正确处理此返回值,就可能会引入错误。然而,作为使用这个第三方库的人,你可能并没接到变更通知。由于 Mock 不会返回这个新的值,所以你的测试代码仍旧成功执行。

所以,尽量不要使用 Mock ,而尽可能使用真实的实现代码。如果行不通的话,至少也应该使用由第三方库维护者所提供的伪实现( fake implimetation )。这会减少上面列出使用 Mock 所带来的维护成本,因为我们使用了真实的实现,上面由于 Mock 带来的维护问题不会发生。例如:

FakeSalaryProcessor fakeProcessor = new FakeSalaryProcessor(); // Designed for tests
MyPaymentService myPaymentService = new MyPaymentService(fakeProcessor);
assertThat(myPaymentService.sendPayment()).isEqualTo(PaymentStatus.SUCCESS);

如果你既不能使用真实的实现,也没有伪实现( 第三方库的维护者没有为你创建一个 fake implementation ),那么,你就要自己创建一个 wrapper 类来模拟 (Mock) 它了。虽然这并不很理想,但通过避免依赖于第三方库 API 的 Mock,也是可以减少一些维护负担。例如:

@Mock MySalaryProcessor mockMySalaryProcessor; // Wraps the SalaryProcessor library
...
// Mock the wrapper class rather than the library itself
when(mockMySalaryProcessor.sendSalary()).thenReturn(PaymentStatus.SUCCESS);

MyPaymentService myPaymentService = new MyPaymentService(mockMySalaryProcessor);
assertThat(myPaymentService.sendPayment()).isEqualTo(PaymentStatus.SUCCESS);

为了避免上面列出的问题,我们更愿意使用对真实实现的调用来测试 Wrapper 类。这样,使用实际实现进行测试的缺点(例如,测试运行时间较长)就被限制在这个 Wrapper 类的测试上,而不是整个代码库的测试。

“不要模拟( Mock )不属于你负责的组件!” 也在 Steve Freeman 和 Nat Pryce 写的 《Growing Object Oriented Software, Guided by Tests》一书中提到了。更多关于过度使用 Mocks 的缺点(甚至你自己的负责的类)可参见 这里.

如果你不想那么麻烦,也可以使用简单使用下面的方式初步判断一下。但是,由于它将问题过于简化了,所以仅作为参考。


原文作者:Stefan Kennedy and Andrew Trenk

原文链接:Don’t Mock Types You Don’t Own

发表时间:July 16, 2020