我最近和一个同事讨论了关于模拟的话题。他说模拟类非常不好,除非在极少数情况下才应该这样做。
他说只有接口应该被模拟,否则就是架构上的错误。
我想知道为什么这个观点(我完全相信他)是如此正确?我不知道,希望能够被说服。
我是否理解错了模拟的要点(是的,我读过Martin Fowler的文章)
我最近和一个同事讨论了关于模拟的话题。他说模拟类非常不好,除非在极少数情况下才应该这样做。
他说只有接口应该被模拟,否则就是架构上的错误。
我想知道为什么这个观点(我完全相信他)是如此正确?我不知道,希望能够被说服。
我是否理解错了模拟的要点(是的,我读过Martin Fowler的文章)
Mocking被用于协议测试 - 它测试你如何使用API以及当API相应时如何作出反应。
在理想情况下(至少在许多情况下),API应该被指定为接口而不是类 - 接口定义了一个协议,类定义了至少部分实现。
就实际情况而言,模拟框架在模拟类方面往往存在限制。
根据我的经验,Mocking有些过度使用 - 通常你并不真正关心精确的交互,你真正需要的是一个存根...但是模拟框架可以用于创建存根,并且通过模拟而不是存根来创建脆弱的测试。然而,这是一个难以平衡的问题。
在我看来,你的同事的意思是,你应该针对接口编程,而不是具体实现。如果你发现自己经常模拟类,那么这表明在设计架构时你违反了之前的原则。
对类进行模拟测试(与模拟接口相比)是不好的,因为模拟仍然有一个真实的类在后台,它是继承来的,可能会在测试期间执行真正的实现。
当你对接口进行模拟(或存根或其他操作)时,就不存在你实际想要模拟的代码被执行的风险。
对类进行模拟测试还会强制你使可能被模拟的所有内容都变成虚拟的,这非常具有侵入性,可能会导致糟糕的类设计。
如果你想解耦类,它们不应该互相知道,这就是为什么对它们中的一个进行模拟(或存根或其他操作)是有意义的原因。因此,实现接口推荐使用,但其他人已经足够提到这一点了。
在开发生命周期的早期编写测试时,使用模拟类是有意义的。
即使具体实现可用,也有继续使用模拟类的趋势。还有一种趋势是在项目早期开发对模拟类(和存根)进行必要的开发,当系统的某些部分尚未构建时。
一旦系统的一部分已经构建完成,就需要针对它进行测试并继续进行回归测试。在这种情况下,从模拟开始是好的,但应尽快放弃它们,转而使用实现。我见过一些项目因为不同的团队继续根据模拟行为而不是实现(一旦可用)而陷入困境。
通过对模拟进行测试,您假定模拟是系统的特征。通常,这涉及猜测模拟组件将要做什么。如果您有系统的规范,则无需猜测,但由于在构建过程中发现了实际考虑因素,因此“按原样构建”的系统通常与原始规范不匹配。敏捷开发项目假设这种情况总会发生。
然后,您开发可以与模拟一起工作的代码。当发现模拟不真正代表实际构建系统的行为(例如,模拟中未见的延迟问题,资源和效率问题,并发问题,性能问题等)时,您将拥有一堆毫无价值的模拟测试,现在必须维护。
我认为在开发开始时使用模拟是有价值的,但这些模拟不应对项目覆盖范围做出贡献。最好在后期删除模拟,并创建适当的集成测试来替换它们,否则您的系统将无法得到测试以检查模拟未模拟(或相对于实际系统模拟不正确)的各种条件。
因此,问题是是否使用模拟,这取决于何时使用它们以及何时删除它们。
关于实践的问题,像大多数问题一样,“这取决于情况”。
过度使用模拟对象可能会导致测试无法真正测试任何内容。它也可能导致测试成为代码重构的虚拟重新实现,与特定实现紧密绑定。
另一方面,恰当地使用模拟对象和存根可以导致单元测试被完全隔离,并且只测试一个事物-这是一件好事。
关键在于适度。
我所说的模拟和存根是由Martin Fowler定义的(定义请见此处),我假设这也是您的同事所想的。
模拟是不好的,因为它可能导致测试过度规范化。尽量使用存根并避免使用模拟。
以下是模拟和存根之间的区别(来自上述文章):
We can then use state verification on the stub like this.
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()); }
Of course this is a very simple test - only that a message has been sent. We've not tested it was send to the right person, or with the right contents, but it will do to illustrate the point.
Using mocks this test would look quite different.
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()); } }
In order to use state verification on the stub, I need to make some extra methods on the >stub to help with verification. As a result the stub implements MailService but adds extra >test methods.