Differenza tra Stub, Mock e Spy nel framework Spock

1. Panoramica

In questo tutorial, discuteremo le differenze tra Mock , Stub e Spy nel framework Spock . Illustreremo cosa offre il framework in relazione al test basato sull'interazione.

Spock è un framework di test per Java e Groovy che aiuta ad automatizzare il processo di test manuale dell'applicazione software. Introduce i propri mock, stub e spie e viene fornito con funzionalità integrate per i test che normalmente richiedono librerie aggiuntive.

Innanzitutto, illustreremo quando dovremmo usare gli stub. Quindi, passeremo attraverso le prese in giro. Alla fine, descriveremo la spia introdotta di recente .

2. Dipendenze di Maven

Prima di iniziare, aggiungiamo le nostre dipendenze Maven:

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

Nota che avremo bisogno della versione 1.3-RC1-groovy-2.5 di Spock. Spy sarà introdotto nella prossima versione stabile di Spock Framework. In questo momento Spy è disponibile nella prima release candidate per la versione 1.3.

Per un riepilogo della struttura di base di un test di Spock, dai un'occhiata al nostro articolo introduttivo sui test con Groovy e Spock.

3. Test basato sull'interazione

Il test basato sull'interazione è una tecnica che ci aiuta a testare il comportamento degli oggetti , in particolare il modo in cui interagiscono tra loro. Per questo, possiamo usare implementazioni fittizie chiamate mock e stub.

Naturalmente, potremmo certamente scrivere molto facilmente le nostre implementazioni di mock e stub. Il problema appare quando la quantità del nostro codice di produzione cresce. Scrivere e mantenere questo codice a mano diventa difficile. Questo è il motivo per cui utilizziamo framework beffardi, che forniscono un modo conciso per descrivere brevemente le interazioni previste. Spock ha il supporto integrato per deridere, stubbing e spiare.

Come la maggior parte delle librerie Java, Spock utilizza il proxy dinamico JDK per le interfacce mocking e proxy Byte Buddy o cglib per le classi mocking. Crea implementazioni fittizie in fase di esecuzione.

Java ha già molte librerie diverse e mature per prendere in giro classi e interfacce. Sebbene ognuno di questi possa essere usato in Spock , c'è ancora una ragione principale per cui dovremmo usare i mock, gli stub e le spie di Spock. Introducendo tutto questo a Spock, possiamo sfruttare tutte le capacità di Groovy per rendere i nostri test più leggibili, più facili da scrivere e decisamente più divertenti!

4. Stubbing Method Calls

A volte, negli unit test, è necessario fornire un comportamento fittizio della classe . Potrebbe essere un client per un servizio esterno o una classe che fornisce l'accesso al database. Questa tecnica è nota come stubbing.

Uno stub è una sostituzione controllabile di una dipendenza di classe esistente nel nostro codice testato. Ciò è utile per effettuare una chiamata al metodo che risponde in un certo modo. Quando usiamo stub, non ci interessa quante volte verrà invocato un metodo. Invece, vogliamo solo dire: restituisci questo valore quando viene chiamato con questi dati.

Passiamo al codice di esempio con logica aziendale.

4.1. Codice in prova

Creiamo una classe modello chiamata Item :

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

Dobbiamo sovrascrivere il metodo equals (Object other) per far funzionare le nostre asserzioni. Spock userà uguale durante le asserzioni quando useremo il doppio segno di uguale (==):

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

Ora creiamo un'interfaccia ItemProvider con un metodo:

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

Avremo bisogno anche di una classe che verrà testata. Aggiungeremo un ItemProvider come dipendenza in 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()); } }

Vogliamo che il nostro codice dipenda da un'astrazione, piuttosto che da un'implementazione specifica. Ecco perché usiamo un'interfaccia. Questo può avere molte implementazioni differenti. Ad esempio, potremmo leggere elementi da un file, creare un client HTTP su un servizio esterno o leggere i dati da un database.

In questo codice, dovremo bloccare la dipendenza esterna, perché vogliamo solo testare la nostra logica contenuta nel metodo getAllItemsSortedByName .

4.2. Utilizzo di un oggetto bloccato nel codice sottoposto a test

Inizializziamo l' oggetto ItemService nel metodo setup () usando uno Stub per la dipendenza ItemProvider :

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

Ora, facciamo in modo che itemProvider restituisca un elenco di elementi a ogni invocazione con l'argomento specifico :

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

Usiamo >> operando per bloccare il metodo. Il metodo getItems restituirà sempre un elenco di due elementi quando viene chiamato con ['offer-id', 'offer-id-2'] list. [] è una scorciatoia di Groovy per creare elenchi.

Ecco l'intero metodo di prova:

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

Ci sono molte altre capacità di stubbing che possiamo usare, come: usare vincoli di corrispondenza degli argomenti, usare sequenze di valori negli stub, definire comportamenti diversi in determinate condizioni e concatenare le risposte del metodo.

5. Metodi di classe beffardi

Ora, parliamo di prendere in giro classi o interfacce in Spock.

A volte, vorremmo sapere se qualche metodo dell'oggetto dipendente è stato chiamato con argomenti specificati . Vogliamo concentrarci sul comportamento degli oggetti ed esplorare come interagiscono osservando le chiamate ai metodi.Il mocking è una descrizione dell'interazione obbligatoria tra gli oggetti nella classe di test.

Testeremo le interazioni nel codice di esempio che abbiamo descritto di seguito.

5.1. Codice con interazione

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

In questo articolo, abbiamo descritto a fondo spie, prese in giro e matrici in Groovy . La conoscenza di questo argomento renderà i nostri test più veloci, affidabili e facili da leggere.

L'implementazione di tutti i nostri esempi può essere trovata nel progetto Github.