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

乔梁 | 2021-04-18

我们公司正在进行一场关于如何以及在什么地方应该使用 Mock 技术来模拟外部组件(库)的讨论。

一方面,对外部组件的模拟( Mocks )让你能够验证系统的边界,而不必使用外部系统。另一方面,使用 Mocks 模仿了外部组件的同时,我们也为测试创建了一个不自然的且有可能偏离了真实的实现的机会。

下面的代码模拟了一个第三方库。这么做,可能会引发什么样的问题呢?

// 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 类来模拟它了。虽然这并不理想,但通过避免依赖于 第三方库 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 类的测试上,而不是整个代码库的测试。

“不要模拟( 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