Difference Between Stub, Mock, and Spy in the Spock Framework – Spock框架中Stub、Mock和Spy之间的区别

最后修改: 2019年 3月 9日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’re going to discuss the differences between Mock, Stub, and Spy in the Spock framework. We’ll illustrate what the framework offers in relation to interaction based testing.

在本教程中,我们将讨论Spock框架中MockStubSpy之间的区别。我们将说明该框架在基于交互的测试方面所提供的功能。

Spock is a testing framework for Java and Groovy that helps automate the process of manual testing of the software application. It introduces its own mocks, stubs, and spies, and comes with built-in capabilities for tests that normally require additional libraries.

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

First, we’ll illustrate when we should use stubs. Then, we’ll go through mocking. In the end, we’ll describe the recently introduced Spy.

首先,我们将说明何时应该使用存根。然后,我们将介绍嘲讽。最后,我们将介绍最近推出的Spy

2. Maven Dependencies

2.Maven的依赖性

Before we start, let’s add our Maven dependencies:

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

<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>

Note that we’ll need the 1.3-RC1-groovy-2.5 version of Spock. Spy will be introduced in the next stable version of Spock Framework. Right now Spy is available in the first release candidate for version 1.3.

请注意,我们将需要1.3-RC1-groovy-2.5版本的Spock。Spy将在Spock框架的下一个稳定版本中引入。现在Spy在1.3版本的第一个候选发布版中可用

For a recap of the basic structure of a Spock test, check out our introductory article on testing with Groovy and Spock.

关于Spock测试的基本结构,请查看我们的关于使用Groovy和Spock测试的介绍性文章

3. Interaction Based Testing

3.基于互动的测试

Interaction-based testing is a technique that helps us test the behavior of objects – specifically, how they interact with each other. For this, we can use dummy implementations called mocks and stubs.

基于交互的测试是一种技术,可以帮助我们测试对象的行为–具体来说,就是它们之间如何交互。为此,我们可以使用称为mocks和stubs的假实现。

Of course, we could certainly very easily write our own implementations of mocks and stubs. The problem appears when the amount of our production code grows. Writing and maintaining this code by hand becomes difficult. This is why we use mocking frameworks, which provide a concise way to briefly describe expected interactions. Spock has built-in support for mocking, stubbing, and spying.

当然,我们当然可以非常容易地编写我们自己的mocks和stubs的实现。当我们的生产代码量增加时,问题就出现了。手工编写和维护这些代码变得很困难。这就是为什么我们要使用mocking框架,它提供了一种简明的方式来简要地描述预期的交互。Spock内置了对嘲讽、存根和监视的支持。

Like most Java libraries, Spock uses JDK dynamic proxy for mocking interfaces and Byte Buddy or cglib proxies for mocking classes. It creates mock implementations at runtime.

与大多数Java库一样,Spock使用JDK动态代理来模拟接口,以及Byte Buddycglib代理来模拟类。它在运行时创建模拟的实现。

Java already has many different and mature libraries for mocking classes and interfaces. Although each of these can be used in Spock, there is still one major reason why we should use Spock mocks, stubs, and spies. By introducing all of these to Spock, we can leverage all of Groovy’s capabilities to make our tests more readable, easier to write, and definitely more fun!

Java已经有很多不同的、成熟的库用于模拟类和接口。虽然这些都可以在Spock中使用,,但仍有一个主要的原因,即我们应该使用Spock的mocks、stubs和spies。通过将所有这些引入Spock,我们可以利用Groovy的所有功能,使我们的测试更可读,更容易编写,而且肯定更有趣

4. Stubbing Method Calls

4.存根的方法调用

Sometimes, in unit tests, we need to provide a dummy behavior of the class. This might be a client for an external service, or a class that provides access to the database. This technique is known as stubbing.

有时,在单元测试中,我们需要提供一个类的虚拟行为。这可能是一个外部服务的客户端,或者是一个提供对数据库访问的类。这种技术被称为 “存根”。

A stub is a controllable replacement of an existing class dependency in our tested code. This is useful for making a method call that responds in a certain way. When we use stub, we don’t care how many times a method will be invoked. Instead, we just want to say: return this value when called with this data.

存根是我们测试代码中对现有类依赖性的可控替换。这对于做一个以某种方式响应的方法调用是很有用的。当我们使用stub时,我们并不关心一个方法会被调用多少次。相反,我们只想说:用这个数据调用时返回这个值。

Let’s move to the example code with business logic.

让我们转到带有业务逻辑的示例代码。

4.1. Code Under Test

4.1 被测试的代码

Let’s create a model class called Item:

让我们创建一个名为Item的模型类。

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

    // standard constructor, getters, equals
}

We need to override the equals(Object other) method to make our assertions work. Spock will use equals during assertions when we use the double equal sign (==):

我们需要覆盖equals(Object other) 方法来使我们的断言工作。当我们使用双等号(==)时,Spock将在断言中使用equals

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

Now, let’s create an interface ItemProvider with one method:

现在,让我们创建一个有一个方法的接口ItemProvider

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

We’ll need also a class that will be tested. We’ll add an ItemProvider as a dependency in ItemService:

我们还需要一个将被测试的类。我们将添加一个ItemProvider作为ItemService:中的一个依赖项。

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

}

We want our code to depend on an abstraction, rather than a specific implementation. That’s why we use an interface. This can have many different implementations. For example, we could read items from a file, create an HTTP client to external service, or read the data from a database.

我们希望我们的代码依赖于一个抽象概念,而不是一个具体的实现。这就是为什么我们使用一个接口。这可以有许多不同的实现。例如,我们可以从一个文件中读取项目,创建一个HTTP客户端到外部服务,或者从数据库中读取数据。

In this code, we’ll need to stub the external dependency, because we only want to test our logic contained in the getAllItemsSortedByName method.

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

4.2. Using a Stubbed Object in the Code Under Test

4.2.在被测代码中使用存根对象

Let’s initialize the ItemService object in the setup() method using a Stub for the ItemProvider dependency:

让我们在setup()方法中使用StubItemProvider依赖性初始化ItemService对象。

ItemProvider itemProvider
ItemService itemService

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

Now, let’s make itemProvider return a list of items on every invocation with the specific argument:

现在,让我们让itemProvider在每次调用特定参数时返回一个项目列表

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

We use >> operand to stub the method. The getItems method will always return a list of two items when called with [‘offer-id’, ‘offer-id-2′] list. [] is a Groovy shortcut for creating lists.

我们使用>>操作数来存根该方法。getItems方法在用[‘offer-id’, ‘offer-id-2’]list. [] 是一个用于创建列表的Groovy快捷方式。

Here’s the whole test method:

这里是整个测试方法。

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

There are many more stubbing capabilities we can use, such as: using argument matching constraints, using sequences of values in stubs, defining different behavior in certain conditions, and chaining method responses.

我们还可以使用更多的存根功能,例如:使用参数匹配约束,在存根中使用数值序列,在某些条件下定义不同的行为,以及连锁方法响应。

5. Mocking Class Methods

5.嘲弄类方法

Now, let’s talk about mocking classes or interfaces in Spock.

现在,让我们来谈谈Spock中的嘲弄类或接口。

Sometimes, we would like to know if some method of the dependent object was called with specified arguments. We want to focus on the behavior of the objects and explore how they interact by looking on the method calls. Mocking is a description of mandatory interaction between the objects in the test class.

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

We’ll test the interactions in the example code we’ve described below.

我们将在下面描述的示例代码中测试相互作用。

5.1. Code with Interaction

5.1.具有交互性的代码

For a simple example, we’re going to save items in the database. After success, we want to publish an event on the message broker about new items in our system.

对于一个简单的例子,我们将在数据库中保存项目。成功后,我们要在消息代理上发布一个关于我们系统中新项目的事件。

The example message broker is a RabbitMQ or Kafkaso generally, we’ll just describe our contract:

示例的消息代理是RabbitMQ或Kafka所以一般来说,我们只是描述我们的合同。

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

Our test method will save non-empty items in the database and then publish the event. Saving item in the database is irrelevant in our example, so we’ll just put a comment:

我们的测试方法将在数据库中保存非空的项目,然后发布该事件。在我们的例子中,在数据库中保存项目是无关紧要的,所以我们只需要放一个注释。

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. Verifying Interaction with Mocked Objects

5.2.验证与模拟对象的交互

Now, let’s test the interaction in our code.

现在,让我们测试一下代码中的交互。

First, we need to mock EventPublisher in our setup() method. So basically, we create a new instance field and mock it by using Mock(Class) function:

首先,我们需要在我们的setup() 方法中模拟EventPublisher。所以基本上,我们创建一个新的实例字段并通过使用Mock(Class)函数来模拟它。

class ItemServiceTest extends Specification {

    ItemProvider itemProvider
    ItemService itemService
    EventPublisher eventPublisher

    def setup() {
        itemProvider = Stub(ItemProvider)
        eventPublisher = Mock(EventPublisher)
        itemService = new ItemService(itemProvider, eventPublisher)
}

Now, we can write our test method. We’ll pass 3 Strings: ”, ‘a’, ‘b’ and we expect that our eventPublisher will publish 2 events with ‘a’ and ‘b’ Strings:

现在,我们可以编写我们的测试方法。我们将传递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')
}

Let’s take a closer look at our assertion in the final then section:

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

1 * eventPublisher.publish('a')

We expect that itemService will call an eventPublisher.publish(String) with ‘a’ as the argument.

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

In stubbing, we’ve talked about argument constraints. Same rules apply to mocks. We can verify that eventPublisher.publish(String) was called twice with any non-null and non-empty argument:

在stubbing中,我们已经谈到了参数约束。同样的规则也适用于mock。我们可以验证eventPublisher.publish(String) 是否被调用了两次,并带有任何非空的参数:

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

5.3. Combining Mocking and Stubbing

5.3.结合Mocking和Stubbing

In Spock, a Mock may behave the same as a Stub. So we can say to mocked objects that, for a given method call, it should return the given data.

Spock中,Mock的行为可能与Stub相同。因此我们可以对被模拟的对象说,对于一个给定的方法调用,它应该返回给定的数据。

Let’s override an ItemProvider with Mock(Class) and create a new ItemService:

让我们用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')]

We can rewrite the stubbing from the given section:

我们可以从给定部分重写存根。

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

So generally, this line says: itemProvider.getItems will be called once with [‘item-‘id’] argument and return given array.

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

We already know that mocks can behave the same as stubs. All of the rules regarding argument constraints, returning multiple values, and side-effects also apply to Mock.

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

6. Spying Classes in Spock

6.斯波克的间谍课

Spies provide the ability to wrap an existing object. This means we can listen in on the conversation between the caller and the real object but retain the original object behavior. Basically, Spy delegates method calls to the original object.

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

In contrast to Mock and Stub, we can’t create a Spy on an interface. It wraps an actual object, so additionally, we will need to pass arguments for the constructor. Otherwise, the type’s default constructor will be invoked.

MockStub相比,我们不能在一个接口上创建一个Spy。它包装了一个实际的对象,所以另外,我们需要为构造函数传递参数。否则,该类型的默认构造函数将被调用。

6.1. Code Under Test

6.1 被测试的代码

Let’s create a simple implementation for EventPublisher. LoggingEventPublisher will print in the console the id of every added item. Here’s the interface method implementation:

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

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

6.2. Testing with Spy

6.2.用Spy测试

We create spies similarly to mocks and stubs, by using the Spy(Class) method. LoggingEventPublisher does not have any other class dependencies, so we don’t have to pass constructor args:

我们通过使用Spy(Class)方法,与mocks和stubs类似地创建间谍。LoggingEventPublisher没有任何其他的类依赖,所以我们不需要传递构造器的args。

eventPublisher = Spy(LoggingEventPublisher)

Now, let’s test our spy. We need a new instance of ItemService with our spied object:

现在,让我们测试一下我们的间谍。我们需要一个新的ItemService的实例,其中有我们的间谍对象。

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

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

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

We verified that the eventPublisher.publish method was called only once. Additionally, the method call was passed to the real object, so we’ll see the output of println in the console:

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

I've published: item-id

Note that when we use stub on a method of Spy, then it won’t call the real object method. Generally, we should avoid using spies. If we have to do it, maybe we should rearrange the code under specification?

请注意,当我们在Spy的方法上使用stub时,那么它将不会调用真正的对象方法。一般来说,我们应该避免使用spies。如果我们必须这样做,也许我们应该重新安排规范下的代码?

7. Good Unit Tests

7.良好的单元测试

Let’s end with a quick summary of how the use of mocked objects improves our tests:

让我们在最后快速总结一下使用模拟对象是如何改进我们的测试的。

  • we create deterministic test suites
  • we won’t have any side effects
  • our unit tests will be very fast
  • we can focus on the logic contained in a single Java class
  • our tests are independent of the environment

8. Conclusion

8.结语

In this article, we thoroughly described spies, mocks, and stubs in Groovy. Knowledge on this subject will make our tests faster, more reliable, and easier to read.

在这篇文章中,我们彻底描述了Groovy中的spies、mocks和stubs关于这个主题的知识将使我们的测试更快、更可靠、更容易阅读。

The implementation of all our examples can be found in the Github project.

我们所有例子的实现都可以在Github项目中找到。