存根,模拟和代理测试
模拟测试是指使用模拟对象代替真实对象的单元测试。实际对象是指测试单元(类)将在实际应用程序中使用的对象。如果我们有一个Calculator类,则需要dao(数据访问对象)对象从数据库中加载所需的数据,则dao对象是"真实对象"。为了测试Calculator类,我们必须为它提供一个与数据库具有有效连接的dao对象。此外,我们还必须将测试所需的数据插入数据库。
设置连接并将数据插入数据库中可能需要很多工作。相反,我们可以为Calculator实例提供伪造的dao类,该类仅返回测试所需的数据。伪dao类实际上不会从数据库中读取数据。伪dao类是一个模拟对象。真实对象的替代品,它使测试Calculator类更加容易。纯粹的模拟测试人员会称这种假冒为存根。稍后我将进行区分。
使用dao对象的计算器的情况可以概括为"使用协作者的单位"。单位是"计算器",而协作者是" dao"对象。我将这样表示:
unit --> collaborator
箭头表示"用途"。当协作者与模拟(或者存根)交换时,将表示为:
unit --> mock
在单元测试情况下,它将如下所示:
unit test --> unit --> collaborator
... 或者...
unit test --> unit --> mock
可以使用三种类型的伪造对象进行测试:存根,模拟和代理。请记住,在单元测试期间,存根,模拟或者代理会替换被测试单元的协作者。存根和模拟物遵循Martin Fowlers对存根和模拟物的定义。
存根,模拟和代理
存根是实现组件接口的对象,但是可以将存根配置为返回适合测试的值,而不是返回调用时组件将返回的内容。使用存根,单元测试可以测试单元是否可以处理来自其协作者的各种返回值。在单元测试中使用存根而不是真正的协作者可以表示为:
首先,单元测试创建存根并配置其返回值。然后,单元测试将创建该单元并在其上设置存根。现在,单元测试将调用该单元,该单元又调用存根。最后,单元测试对单元上方法调用的结果进行断言。
- 单元测试->存根
- 单元测试->单元->存根
- 单元测试根据单元的结果和状态进行断言
Mock就像一个存根,只有它也有一些方法可以确定在Mock上调用什么方法。因此,可以使用模拟程序测试单元是否可以正确处理各种返回值,以及单元是否正确使用了协作者。例如,我们无法通过dao对象返回的值来查看是否使用Statement或者PreparedStatement从数据库中读取了数据。我们也看不到在返回值之前是否调用了connection.close()方法。模拟可以做到这一点。换句话说,模拟使测试单元与协作者的完整交互成为可能。不仅返回单位使用的值的协作方法。在单元测试中使用模拟可以表示为:
首先,单元测试创建模拟并配置其返回值。然后,单元测试将创建该单元并在其上设置模拟。现在,单元测试调用该单元,该单元又调用该模拟。最后,单元测试对单元上方法调用的结果进行断言。单元测试还可以对模拟中调用的方法进行断言。
- 单元测试->模拟
- 单元测试->单元->模拟
- 单元测试对单元的结果和状态进行断言
- 单元测试对模拟调用的方法进行断言
模拟测试中的代理是模拟对象,它们将方法调用委派给实际的协作者对象,但仍在内部记录代理上调用的方法。因此,代理使与真正的合作者进行模拟测试成为可能。在单元测试中使用代理可以表示为:
首先,单元测试将创建协作者。然后,单元测试将为协作者创建代理。第三,单元测试创建单元并在其上设置代理。现在,单元测试调用该单元,该单元又调用代理。最后,单元测试对单元上方法调用的结果进行断言。单元测试还对在代理服务器上调用了什么方法进行断言。
- 单元测试->合作者
- 单元测试->代理
- 单元测试->单元->代理->协作者
- 单元测试对单元的结果和状态进行断言
- 单元测试对在代理上调用的方法进行断言
有几种流行的模拟测试API。其中包括JMock和EasyMock。撰写本文时,这两个API不支持如上所述的代理。注意:它们*使用* java.lang.reflect.Proxy实例来实现其动态模拟。但这与上述测试代理不同。这些API仅可用于存根和模拟,而不能用作真正的协作者的代理。我确定他们会在将来添加它。
对于本文中的代码示例,我将使用自己的测试API蝴蝶测试工具。我开发了Butterfly Testing Tools,因为我需要JMock和EasyMock都没有的代理测试函数。在我看来,API的设计可以更灵活,更灵活(我想这是个人风格的问题)。
带有见证的存根,模拟和代理测试
首先,让我们看看如何为接口创建存根:
连接变量是Connection接口的存根。现在,我可以在连接实例上调用方法,就像它是真实的数据库连接一样。但是,由于未将存根配置为返回任何特殊值,因此这些方法将仅返回null。为了配置存根以返回适合测试的值,我们必须获得存根的Mock。这是完成的方式:
现在,我们具有与存根关联的模拟了。 IMock界面具有的方法之一是
Connection connection = (Connection) MockFactory.createProxy(Connection.class);
使用addReturnValue方法可以将返回值添加到存根。我们可以根据需要添加任意数量。返回值将从存根中按添加顺序相同的顺序返回。一旦返回了返回值,就将其从存根中删除,就像在队列中一样。注意:添加的返回值的顺序必须与存根上的调用方法的顺序相匹配!如果将字符串" myReturnValue"作为返回值添加到存根,然后调用connection.prepareStatement(" select * fromhouses")返回PreparedStatement,则会得到异常。无法从connection.prepareStatement(" ...");返回String返回值。我们将必须确保自己,存根上的返回值和调用的方法匹配。
IMock mock = MockFactory.getMock(connection);
在具有存根的IMock实例后,我们还可以假设在存根上调用了哪些方法。为此,IMock接口具有一系列assertCalled(MethodInvocation)方法。这是一个例子:
addReturnValue(Object returnValue);
如果尚未调用connection.close()方法,则抛出java.lang.AssertionError。如果将JUnit用于单元测试,则JUnit将捕获AssertionError并报告测试失败。 IMock接口上还有其他assertCalled()等方法。有关更多信息,请参见见证项目。
我将展示的最后一件事是如何为真实连接创建代理并获取与代理关联的模拟:
mock.assertCalled(new MethodInvocation("close"));
很简单,不是吗?它与为接口创建存根相同。我们只需将真正的协作者提供给MockFactory即可,而不是提供接口(类对象)。在将方法调用转发到realConnection实例之前,proxyConnection将记录对其调用的所有方法。这样,我们就可以像使用真正的连接一样使用proxyConnection,同时对所调用的方法进行模拟断言。
我们甚至可以通过模拟.addReturnValue(...)向代理添加返回值来临时将proxyConnection变成存根。当proxyConnection看到返回值时,它将返回该值,而不是将调用转发给realConnection。一旦返回了所有返回值,proxyConnection将继续将方法调用转发给realConnection实例。这样,我们可以在将proxyConnection用作真实连接或者存根之间进行切换。聪明,不是吗?
//opens a real database connection. Connection realConnection = getConnection(); Connection proxyConnection = MockFactory.createProxy(realConnection); IMock mock = MockFactory.getMock(proxyConnection);
不仅数据库连接对模拟很有用。在测试过程中,可能会嘲笑被测单元的任何协作者。是否使用模拟或者真实的协作者测试单元取决于情况。代理使得可以同时执行这两个操作,甚至在执行过程中对一些方法调用进行了存根处理。