Spock框架中Stub、Mock和Spy的区别

2023/05/12

1. 概述

在本教程中,我们介绍Spock框架中Mock、Stub和Spy之间的区别,我们会说明框架提供的与基于交互的测试相关的内容。

Spock是一个用于Java和Groovy的测试框架,有助于自动化软件应用程序的手动测试过程,它引入了自己的mocks、stubs和spies,并为通常需要额外库的测试提供了内置功能。

首先,我们会说明何时应该使用Stubs,然后,我们会进行Mock,最后,我们会介绍后面推出的Spy。

2. Maven依赖

在开始之前,首先添加我们的Maven依赖项:

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.3-groovy-2.4</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版本,Spy将在Spock框架的下一个稳定版本中引入,现在Spy在1.3版本的第一个候选版本中可用

3. 基于交互的测试

基于交互的测试是一种帮助我们测试对象行为的技术,特别是它们如何相互交互。为此,我们可以使用称为mock和stub的虚拟实现。

当然,我们可以很容易地编写自己的mock和stub实现,但当我们的生产代码量增加时,问题就出现了。手动编写和维护此代码变得很困难,这就是为什么我们要使用mock框架的原因,它提供了一种简洁的方式来简要描述预期的交互。Spock内置了对mock、stub和spy的支持

与大多数Java库一样,Spock使用JDK动态代理来mock接口,使用Byte Buddy或cglib代理来mock类,它在运行时创建mock实现。

Java已经有许多用于mock类和接口的不同且成熟的库,尽管这些都可以在Spock中使用,但我们应该使用Spock中的mock、stub和spy仍然有一个主要原因,通过将所有这些引入Spock,我们可以利用Groovy的所有功能来使我们的测试更具可读性、更容易编写,而且绝对更有趣。

4. Stubbing方法调用

有时,在单元测试中,我们需要提供类的虚拟行为。这可能是外部服务的客户端,也可能是提供对数据库访问的类,这种技术称为stub。

stub是我们测试代码中现有类依赖项的可控替换,这对于以特定方式响应的方法调用很有用。当我们使用stub时,我们并不关心一个方法会被调用多少次。相反,我们想表达的只是:“当使用此数据调用时返回此值”。

4.1 被测代码

首先我们创建一个名为Item的模型类:

public class Item {
    private final String id;
    private final String name;

    // standard constructor, getters, equals
}

我们需要重写equals(Object other)方法来使我们的断言起作用,当我们使用双等号(==)时,Spock将在断言期间使用equals

new Item('1', 'name') == new Item('1', 'name')

接下来我们创建一个接口ItemProvider:

public interface ItemProvider {
    List<Item> getItems(List<String> itemIds);
}

我们还需要一个将被测试的类,因此创建一个ItemService类,并添加一个ItemProvider作为依赖项:

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());
    }
}

我们希望我们的代码依赖于抽象,而不是特定的实现,这就是我们使用接口的原因,它可以有许多不同的实现。例如,我们可以从文件中读取Item,为外部服务创建HTTP客户端,或者从数据库中读取数据。

在这段代码中,我们需要stub外部依赖项,因为我们只想测试包含在getAllItemsSortedByName方法中的逻辑

4.2 在被测代码中使用stub对象

我们使用ItemProvider依赖项的Stub在setup()方法中初始化ItemService对象:

class ItemServiceUnitTest extends Specification {

	ItemProvider itemProvider
	ItemService itemService

	def setup() {
		itemProvider = Stub(ItemProvider)
		itemService = new ItemService(itemProvider)
	}
}

现在,让itemProvider在每次调用时返回一个带有特定参数的Item集合

itemProvider.getItems(['offer-id', 'offer-id-2']) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]

我们使用“»”操作数来stub该方法,当使用[‘offer-id’, ‘offer-id-2’]集合调用时,getItems方法将始终返回包含两个Item的集合。[]是用于创建集合的Groovy快捷方式。

下面是整个测试方法:

def 'should return items sorted by name'() {
	given:
	def ids = ['offer-id', 'offer-id-2']
	itemProvider.getItems(ids) >> [
			new Item('offer-id', 'Zname'),
			new Item('offer-id-2', 'Aname')
	]
    
	when:
	List<Item> items = itemService.getAllItemsSortedByName(ids)
    
	then:
	items.collect { it.name } == ['Aname', 'Zname']
}

我们可以使用更多的stub功能,例如:使用参数匹配约束、使用stub中的值序列、在特定条件下定义不同的行为以及链接方法响应。

5. Mock类方法

有时,我们想知道是否使用指定的参数调用了依赖对象的某个方法,我们希望专注于对象的行为,并通过查看方法调用来探索它们如何交互。mock是对测试类中对象之间强制交互的描述。

5.1 交互代码

举个简单的例子,我们将在数据库中保存Item,保存成功后,我们想在消息服务上发布一个关于我们系统中新Item的事件。

常用的消息服务是RabbitMQ或Kafka,所以一般来说,我们只描述我们的合约:

public interface EventPublisher {
    void publish(String addedOfferId);
}

我们的测试方法会将非空Item保存在数据库中,然后发布事件。在我们的示例中,将Item保不保存在数据库中是无关紧要的,重点是发布事件。因此为了简单起见,跳过实际的数据库操作:

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 验证与mock对象的交互

现在,让我们测试代码中的交互。首先,我们需要在setup()方法中mock EventPublisher,所以我们创建一个新的实例字段并使用Mock(Class)函数mock它:

class ItemServiceUnitTest 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将发布2个带有’a’和’b’字符串的事件:

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')
}

让我们仔细看看在最后then部分中的断言:

1  eventPublisher.publish('a')

我们期望itemService会调用一次eventPublisher.publish(String),参数为’a’。

在stub中,我们讨论了参数约束。相同的规则也适用于mock,我们可以验证是否使用任何非null和非空参数调用了eventPublisher.publish(String)两次

2  eventPublisher.publish({ it != null && !it.isEmpty() })

5.3 结合mock和stub

在Spock中,Mock的行为可能与Stub相同。所以我们可以对mock对象说,对于给定的方法调用,它应该返回给定的数据。

让我们用Mock(Class)覆盖ItemProvider并创建一个新的ItemService:

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')]

我们可以从给定的部分重写stub:

1  itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]

所以一般来说,这一行说:itemProvider.getItems将使用[‘item-‘id’]参数调用一次并返回给定的数组

我们已经知道mock的行为与stub相同。所有关于参数约束、返回多个值和副作用的规则也适用于Mock。

6. Spock中的Spy类

Spy提供了包装现有对象的能力,这意味着我们可以监听调用者和真实对象之间的对话,但保留原始对象行为。基本上,Spy将方法调用委托给原始对象

与Mock和Stub不同,我们不能在接口上创建Spy。它包装了一个实际的对象,因此另外,我们需要为构造函数传递参数。否则,将调用该类型的默认构造函数。

6.1 被测代码

让我们为EventPublisher创建一个简单的实现LoggingEventPublisher,它将在控制台中打印每个添加Item的id,下面是接口方法的实现:

@Override
public void publish(String addedOfferId) {
    System.out.println("I've published: " + addedOfferId);
}

6.2 使用Spy进行测试

我们使用Spy(Class)方法创建类似于mock和stub的Spy(间谍),LoggingEventPublisher没有任何其他类依赖项,因此我们不必传递构造函数参数:

eventPublisher = Spy(LoggingEventPublisher)

现在,让我们测试一下我们的Spy,我们需要一个带有Spy对象的ItemService新实例:

given:
eventPublisher = Spy(LoggingEventPublisher)
itemService = new ItemService(itemProvider, eventPublisher)

when:
itemService.saveItems(['item-id'])

then:
1  eventPublisher.publish('item-id')

我们验证了eventPublisher.publish方法只被调用了一次。此外,方法调用被传递给了真实对象,所以我们将在控制台中看到println的输出

I've published: item-id

请注意,当我们在Spy的方法上使用stub时,它不会调用真实对象方法。一般来说,我们应该避免使用Spy,如果我们到了必须使用Spy的地步,那也可能意味着我们应该根据规范重新安排代码。

7. 好的单元测试

让我们快速总结一下使用mock对象如何改进我们的测试:

  • 我们创建确定性测试套件
  • 我们不会有任何副作用
  • 我们的单元测试会非常快
  • 我们可以专注于单个Java类中包含的逻辑
  • 我们的测试独立于环境

8. 总结

在本文中,我们详细描述了Groovy中的Spy、Mock和Stub,这些技术的使用可以使我们的测试更快、更可靠、更容易阅读。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章