Difference Between Stub, Mock, and Spy in the Spock Framework
1.概述
在本教程中,我们将讨论Spock框架中Mock,Stub和Spy之间的区别。 我们将说明与基于交互的测试有关的框架。
Spock是Java和Groovy的测试框架,可帮助自动化软件应用程序的手动测试过程。 它引入了自己的模拟,存根和间谍,并具有内置功能,用于通常需要其他库的测试。
首先,我们将说明何时应该使用存根。 然后,我们将进行模拟。 最后,我们将介绍最近引入的Spy。
2. Maven依赖
在开始之前,让我们添加我们的Maven依赖项:
1 2 3 4 5 6 7 8 9 10 11 12 | <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <version>1.3-RC1-groovy-2.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.7</version> <scope>test</scope> </dependency> |
请注意,我们需要Spock的1.3-RC1-groovy-2.5版本。 间谍将在Spock Framework的下一个稳定版本中引入。 目前,Spy在版本1.3的第一个候选版本中可用。
要回顾一下Spock测试的基本结构,请查看有关使用Groovy和Spock进行测试的介绍性文章。
3.基于交互的测试
基于交互的测试是一种技术,可以帮助我们测试对象的行为,特别是对象之间的交互方式。 为此,我们可以使用称为模拟和存根的虚拟实现。
当然,我们当然可以非常容易地编写自己的模拟和存根实现。 当我们的生产代码数量增加时,就会出现问题。 手工编写和维护此代码变得困难。 这就是为什么我们使用模拟框架,该框架提供了简要描述预期交互的简洁方法。 Spock具有对模拟,存根和间谍的内置支持。
与大多数Java库一样,Spock使用JDK动态代理来模拟接口,并使用Byte Buddy或cglib代理来模拟类。 它在运行时创建模拟实现。
Java已经有许多不同且成熟的库用于模拟类和接口。 尽管可以在Spock中使用它们中的每一个,但是仍有一个主要的原因为什么我们应该使用Spock模拟,存根和间谍。 通过将所有这些功能引入Spock,我们可以利用Groovy的所有功能来使我们的测试更具可读性,易于编写,并且绝对更加有趣!
4.存根方法调用
有时,在单元测试中,我们需要提供类的虚拟行为。 这可能是外部服务的客户端,或者是提供对数据库访问权限的类。 此技术称为存根。
存根是我们测试代码中现有类依赖关系的可控替代。 这对于进行以某种方式响应的方法调用很有用。 当使用存根时,我们不在乎将多少次调用一个方法。 相反,我们只想说:用此数据调用时返回此值。
让我们转到具有业务逻辑的示例代码。
4.1。 被测代码
让我们创建一个名为Item的模型类:
1 2 3 4 5 6 |
我们需要重写equals(Object other)方法以使声明生效。 当我们使用双等号(==)时,Spock将在声明期间使用等号:
1 | new Item('1', 'name') == new Item('1', 'name') |
现在,让我们使用一种方法创建一个接口ItemProvider:
1 2 3 | public interface ItemProvider { List<Item> getItems(List<String> itemIds); } |
我们还需要一个将要测试的类。 我们将在ItemService中添加一个ItemProvider作为依赖项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ItemService { private final ItemProvider itemProvider; public ItemService(ItemProvider itemProvider) { this.itemProvider = itemProvider; } List<Item> getAllItemsSortedByName(List<String> itemIds) { List<Item> items = itemProvider.getItems(itemIds); return items.stream() .sorted(Comparator.comparing(Item::getName)) .collect(Collectors.toList()); } } |
我们希望我们的代码依赖于抽象,而不是特定的实现。 这就是为什么我们使用接口。 这可以有许多不同的实现。 例如,我们可以从文件中读取项目,创建外部服务的HTTP客户端或从数据库中读取数据。
在此代码中,我们将需要对外部依赖进行存根,因为我们只想测试getAllItemsSortedByName方法中包含的逻辑。
4.2。 在被测代码中使用存根对象
让我们使用Stub的ItemProvider依赖项在setup()方法中初始化ItemService对象:
1 2 3 4 5 6 7 | ItemProvider itemProvider ItemService itemService def setup() { itemProvider = Stub(ItemProvider) itemService = new ItemService(itemProvider) } |
现在,让我们让itemProvider在每次调用时使用特定参数返回一个项目列表:
1 2 | itemProvider.getItems(['offer-id', 'offer-id-2']) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')] |
我们使用>>操作数对方法进行存根。 当使用['offer-id','offer-id-2'] list调用时,getItems方法将始终返回两个项目的列表。 []是用于创建列表的Groovy快捷方式。
这是整个测试方法:
1 2 3 4 5 6 7 8 9 10 11 | def 'should return items sorted by name'() { given: def ids = ['offer-id', 'offer-id-2'] itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')] when: List<Item> items = itemService.getAllItemsSortedByName(ids) then: items.collect { it.name } == ['Aname', 'Zname'] } |
我们可以使用更多的存根功能,例如:使用参数匹配约束,在存根中使用值序列,在某些条件下定义不同的行为以及链接方法响应。
5.模拟类方法
现在,让我们讨论一下Spock中的模拟类或接口。
有时,我们想知道是否使用指定的参数调用了依赖对象的某些方法。 我们要关注对象的行为,并通过查看方法调用来探索它们之间的交互。模拟是对测试类中对象之间强制交互的描述。
我们将在下面描述的示例代码中测试交互。
5.1。 交互代码
举一个简单的例子,我们将项目保存在数据库中。 成功之后,我们希望在消息代理上发布有关系统中新项目的事件。
消息代理示例是RabbitMQ或Kafka,因此通常,我们仅描述我们的合同:
1 2 3 |
我们的测试方法将非空项目保存在数据库中,然后发布事件。 在我们的示例中,将项目保存到数据库中是无关紧要的,因此,我们将只发表一条评论:
1 2 3 4 5 6 7 8 9 | void saveItems(List<String> itemIds) { List<String> notEmptyOfferIds = itemIds.stream() .filter(itemId -> !itemId.isEmpty()) .collect(Collectors.toList()); // save in database notEmptyOfferIds.forEach(eventPublisher::publish); } |
5.2。 验证与模拟对象的交互
现在,让我们测试代码中的交互。
首先,我们需要在setup()方法中模拟EventPublisher。 因此,基本上,我们创建了一个新的实例字段,并使用Mock(Class)函数对其进行了模拟:
1 2 3 4 5 6 7 8 9 10 11 | class ItemServiceTest extends Specification { ItemProvider itemProvider ItemService itemService EventPublisher eventPublisher def setup() { itemProvider = Stub(ItemProvider) eventPublisher = Mock(EventPublisher) itemService = new ItemService(itemProvider, eventPublisher) } |
现在,我们可以编写测试方法了。 我们将传递3个字符串:",'a','b',并且我们希望eventPublisher将发布两个带有'a'和'b'字符串的事件:
1 2 3 4 5 6 7 8 9 10 11 | def 'should publish events about new non-empty saved offers'() { given: def offerIds = ['', 'a', 'b'] when: itemService.saveItems(offerIds) then: 1 * eventPublisher.publish('a') 1 * eventPublisher.publish('b') } |
让我们在最后的最后一节中仔细研究一下我们的断言:
1 | 1 * eventPublisher.publish('a') |
我们期望itemService将调用一个以'a'作为参数的eventPublisher.publish(String)。
在存根中,我们讨论了参数约束。 相同的规则适用于模拟。 我们可以验证是否使用任何非null和非空参数两次调用eventPublisher.publish(String):
1 | 2 * eventPublisher.publish({ it != null && !it.isEmpty() }) |
5.3。 模拟和存根相结合
在Spock中,Mock的行为可能与Stub相同。 因此,我们可以对模拟对象说,对于给定的方法调用,它应该返回给定的数据。
让我们使用Mock(Class)覆盖ItemProvider并创建一个新的ItemService:
1 2 3 4 5 6 7 8 9 10 | given: itemProvider = Mock(ItemProvider) itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')] itemService = new ItemService(itemProvider, eventPublisher) when: def items = itemService.getAllItemsSortedByName(['item-id']) then: items == [new Item('item-id', 'name')] |
我们可以重写给定部分中的存根:
1 | 1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')] |
因此,通常来说,这一行说:itemProvider.getItems将使用['item-'id']参数调用一次,并返回给定的数组。
我们已经知道,模拟的行为与存根相同。 有关参数约束,返回多个值和副作用的所有规则也适用于Mock。
6. Spock的间谍班
间谍提供了包装现有对象的功能。 这意味着我们可以侦听调用者和真实对象之间的对话,但保留原始对象的行为。 基本上,Spydelegates方法调用原始对象。
与Mockand Stub相比,我们无法创建Spyon接口。 它包装了一个实际的对象,因此,此外,我们将需要为构造函数传递参数。 否则,将调用该类型的默认构造函数。
6.1。 被测代码
让我们为EventPublisher创建一个简单的实现。 LoggingEventPublisher将在控制台中打印每个已添加项目的ID。 这是接口方法的实现:
1 2 3 4 |
6.2。 用间谍测试
通过使用Spy(Class)方法,我们创建了类似于模拟和存根的间谍。 LoggingEventPublisher没有其他任何类依赖关系,因此我们不必传递构造函数args:
1 | eventPublisher = Spy(LoggingEventPublisher) |
现在,让我们测试一下我们的间谍。 我们需要带有我们的监视对象的ItemService的新实例:
1 2 3 4 5 6 7 8 9 | given: eventPublisher = Spy(LoggingEventPublisher) itemService = new ItemService(itemProvider, eventPublisher) when: itemService.saveItems(['item-id']) then: 1 * eventPublisher.publish('item-id') |
我们验证了eventPublisher.publish方法仅被调用一次。 此外,该方法调用已传递给实际对象,因此我们将在控制台中看到println的输出:
1 | I've published: item-id |
请注意,当我们在Spy方法上使用存根时,它将不会调用真实对象方法。 通常,我们应该避免使用间谍。 如果必须这样做,也许我们应该按照规范重新排列代码,
7.好的单元测试
让我们最后简要概述一下使用模拟对象如何改善我们的测试:
我们创建确定性测试套件
我们不会有任何副作用
我们的单元测试将非常快
我们可以专注于单个Java类中包含的逻辑
我们的测试与环境无关
8.结论
在本文中,我们彻底描述了Groovy中的间谍,模拟和存根。 有关此主题的知识将使我们的测试更快,更可靠并且更易于阅读。
我们所有示例的实现都可以在Github项目中找到。