Différence entre Stub, Mock et Spy dans le framework Spock

1. Vue d'ensemble

Dans ce didacticiel, nous allons discuter des différences entre Mock , Stub et Spy dans le framework Spock . Nous illustrerons ce que le framework offre en ce qui concerne les tests basés sur les interactions.

Spock est un framework de test pour Java et Groovy qui permet d'automatiser le processus de test manuel de l'application logicielle. Il introduit ses propres simulacres, stubs et espions, et est livré avec des capacités intégrées pour les tests qui nécessitent normalement des bibliothèques supplémentaires.

Tout d'abord, nous allons illustrer quand nous devrions utiliser des stubs. Ensuite, nous passerons par la moquerie. À la fin, nous décrirons le Spy récemment introduit .

2. Dépendances de Maven

Avant de commencer, ajoutons nos dépendances Maven:

 org.spockframework spock-core 1.3-RC1-groovy-2.5 test   org.codehaus.groovy groovy-all 2.4.7 test 

Notez que nous aurons besoin de la version 1.3-RC1-groovy-2.5 de Spock. Spy sera introduit dans la prochaine version stable de Spock Framework. Actuellement, Spy est disponible dans la première version candidate pour la version 1.3.

Pour un récapitulatif de la structure de base d'un test Spock, consultez notre article d'introduction sur les tests avec Groovy et Spock.

3. Test basé sur l'interaction

Le test basé sur l'interaction est une technique qui nous aide à tester le comportement des objets , en particulier la manière dont ils interagissent les uns avec les autres. Pour cela, nous pouvons utiliser des implémentations factices appelées mocks et stubs.

Bien sûr, nous pourrions certainement écrire très facilement nos propres implémentations de simulations et de stubs. Le problème apparaît lorsque la quantité de notre code de production augmente. Écrire et maintenir ce code à la main devient difficile. C'est pourquoi nous utilisons des frameworks moqueurs, qui fournissent un moyen concis de décrire brièvement les interactions attendues. Spock a un support intégré pour la moquerie, le stubbing et l'espionnage.

Comme la plupart des bibliothèques Java, Spock utilise le proxy dynamique JDK pour les interfaces simulées et les proxys Byte Buddy ou cglib pour les classes simulées. Il crée des implémentations simulées au moment de l'exécution.

Java a déjà de nombreuses bibliothèques différentes et matures pour les classes et les interfaces moqueuses. Bien que chacun d'entre eux puisse être utilisé dans Spock , il y a encore une raison majeure pour laquelle nous devrions utiliser des simulacres, des talons et des espions de Spock. En introduisant tout cela dans Spock, nous pouvons tirer parti de toutes les capacités de Groovy pour rendre nos tests plus lisibles, plus faciles à écrire et certainement plus amusants!

4. Appels de méthode de stubbing

Parfois, dans les tests unitaires, nous devons fournir un comportement factice de la classe . Il peut s'agir d'un client pour un service externe ou d'une classe qui fournit un accès à la base de données. Cette technique est connue sous le nom de stubbing.

Un stub est un remplacement contrôlable d'une dépendance de classe existante dans notre code testé. Ceci est utile pour faire un appel de méthode qui répond d'une certaine manière. Lorsque nous utilisons stub, nous ne nous soucions pas du nombre de fois qu'une méthode sera invoquée. Au lieu de cela, nous voulons simplement dire: renvoie cette valeur lorsqu'elle est appelée avec ces données.

Passons à l'exemple de code avec la logique métier.

4.1. Code en cours de test

Créons une classe de modèle appelée Item :

public class Item { private final String id; private final String name; // standard constructor, getters, equals }

Nous devons remplacer la méthode equals (Object other) pour que nos assertions fonctionnent. Spock utilisera des égaux lors des assertions lorsque nous utilisons le double signe égal (==):

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

Maintenant, créons une interface ItemProvider avec une méthode:

public interface ItemProvider { List getItems(List itemIds); }

Nous aurons également besoin d'une classe qui sera testée. Nous ajouterons un ItemProvider en tant que dépendance dans ItemService:

public class ItemService { private final ItemProvider itemProvider; public ItemService(ItemProvider itemProvider) { this.itemProvider = itemProvider; } List getAllItemsSortedByName(List itemIds) { List items = itemProvider.getItems(itemIds); return items.stream() .sorted(Comparator.comparing(Item::getName)) .collect(Collectors.toList()); } }

Nous voulons que notre code dépende d'une abstraction plutôt que d'une implémentation spécifique. C'est pourquoi nous utilisons une interface. Cela peut avoir de nombreuses implémentations différentes. Par exemple, nous pourrions lire des éléments à partir d'un fichier, créer un client HTTP vers un service externe ou lire les données d'une base de données.

Dans ce code, nous aurons besoin de stuber la dépendance externe, car nous voulons uniquement tester notre logique contenue dans la méthode getAllItemsSortedByName .

4.2. Utilisation d'un objet stubbed dans le code en cours de test

Initialisons l' objet ItemService dans la méthode setup () en utilisant un stub pour la dépendance ItemProvider :

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

Maintenant, faisons que itemProvider renvoie une liste d'éléments à chaque appel avec l'argument spécifique :

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

Nous utilisons >> opérande pour stuber la méthode. La méthode getItems retournera toujours une liste de deux éléments lorsqu'elle est appelée avec ['offer-id', 'offer-id-2'] list. [] est un raccourci Groovy pour créer des listes.

Voici toute la méthode de test:

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 items = itemService.getAllItemsSortedByName(ids) then: items.collect { it.name } == ['Aname', 'Zname'] }

Il existe de nombreuses autres capacités de stubbing que nous pouvons utiliser, telles que: l'utilisation de contraintes de correspondance d'arguments, l'utilisation de séquences de valeurs dans les stubs, la définition de comportements différents dans certaines conditions et le chaînage des réponses des méthodes.

5. Méthodes de classe moqueuse

Parlons maintenant des classes ou des interfaces moqueuses dans Spock.

Parfois, nous aimerions savoir si une méthode de l'objet dépendant a été appelée avec des arguments spécifiés . Nous voulons nous concentrer sur le comportement des objets et explorer comment ils interagissent en regardant les appels de méthode.La moquerie est une description de l'interaction obligatoire entre les objets de la classe de test.

Nous allons tester les interactions dans l'exemple de code que nous avons décrit ci-dessous.

5.1. Code avec interaction

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 Kafka, so generally, we'll just describe our contract:

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 itemIds) { List notEmptyOfferIds = itemIds.stream() .filter(itemId -> !itemId.isEmpty()) .collect(Collectors.toList()); // save in database notEmptyOfferIds.forEach(eventPublisher::publish); }

5.2. Verifying Interaction with Mocked Objects

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:

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:

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:

1 * eventPublisher.publish('a')

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

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:

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

5.3. Combining Mocking and 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.

Let's override an ItemProvider with Mock(Class) and create a new 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.

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.

6. Spying Classes in Spock

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.

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.

6.1. Code Under Test

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:

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

6.2. Testing with 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:

eventPublisher = Spy(LoggingEventPublisher)

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

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:

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?

7. Good Unit Tests

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

Dans cet article, nous avons décrit en détail les espions, les moqueries et les talons de Groovy . La connaissance de ce sujet rendra nos tests plus rapides, plus fiables et plus faciles à lire.

L'implémentation de tous nos exemples se trouve dans le projet Github.