| 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