Unterschied zwischen Stub, Mock und Spy im Spock Framework

1. Übersicht

In diesem Tutorial werden wir die Unterschiede zwischen Mock , Stub und Spy im Spock-Framework diskutieren . Wir werden veranschaulichen, was das Framework in Bezug auf interaktionsbasierte Tests bietet.

Spock ist ein Testframework für Java und Groovy , mit dem der Prozess des manuellen Testens der Softwareanwendung automatisiert werden kann. Es führt seine eigenen Mocks, Stubs und Spione ein und verfügt über integrierte Funktionen für Tests, für die normalerweise zusätzliche Bibliotheken erforderlich sind.

Zuerst werden wir veranschaulichen, wann wir Stubs verwenden sollten. Dann werden wir uns lustig machen. Am Ende werden wir den kürzlich vorgestellten Spion beschreiben .

2. Maven-Abhängigkeiten

Bevor wir beginnen, fügen wir unsere Maven-Abhängigkeiten hinzu:

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

Beachten Sie, dass wir die Version 1.3-RC1-groovy-2.5 von Spock benötigen . Spy wird in der nächsten stabilen Version von Spock Framework eingeführt. Gerade jetzt Spy ist in den ersten Release Candidate für Version 1.3.

Eine Zusammenfassung der Grundstruktur eines Spock-Tests finden Sie in unserem Einführungsartikel zum Testen mit Groovy und Spock.

3. Interaktionsbasiertes Testen

Interaktionsbasiertes Testen ist eine Technik, mit der wir das Verhalten von Objekten testen können - insbesondere, wie sie miteinander interagieren. Hierfür können wir Dummy-Implementierungen verwenden, die als Mocks und Stubs bezeichnet werden.

Natürlich könnten wir sicherlich sehr leicht unsere eigenen Implementierungen von Mocks und Stubs schreiben. Das Problem tritt auf, wenn die Menge unseres Produktionscodes zunimmt. Das Schreiben und Verwalten dieses Codes von Hand wird schwierig. Aus diesem Grund verwenden wir spöttische Frameworks, die eine kurze Möglichkeit bieten, erwartete Interaktionen kurz zu beschreiben. Spock verfügt über eine integrierte Unterstützung zum Verspotten, Stubben und Spionieren.

Wie die meisten Java-Bibliotheken verwendet Spock den dynamischen JDK-Proxy zum Verspotten von Schnittstellen und Byte Buddy- oder cglib-Proxys zum Verspotten von Klassen. Es erstellt zur Laufzeit Scheinimplementierungen.

Java verfügt bereits über viele verschiedene und ausgereifte Bibliotheken zum Verspotten von Klassen und Schnittstellen. Obwohl jedes dieser Elemente in Spock verwendet werden kann , gibt es immer noch einen Hauptgrund, warum wir Spock-Mocks, Stubs und Spione verwenden sollten. Durch die Einführung all dieser Funktionen in Spock können wir alle Funktionen von Groovy nutzen , um unsere Tests lesbarer, einfacher zu schreiben und auf jeden Fall unterhaltsamer zu machen!

4. Stubbing-Methodenaufrufe

Manchmal müssen wir in Unit-Tests ein Dummy-Verhalten der Klasse bereitstellen . Dies kann ein Client für einen externen Dienst oder eine Klasse sein, die Zugriff auf die Datenbank bietet. Diese Technik wird als Stubbing bezeichnet.

Ein Stub ist eine steuerbare Ersetzung einer vorhandenen Klassenabhängigkeit in unserem getesteten Code. Dies ist nützlich, um einen Methodenaufruf durchzuführen, der auf eine bestimmte Weise reagiert. Wenn wir stub verwenden, ist es uns egal, wie oft eine Methode aufgerufen wird. Stattdessen möchten wir nur sagen: Geben Sie diesen Wert zurück, wenn Sie mit diesen Daten aufgerufen werden.

Gehen wir zum Beispielcode mit Geschäftslogik über.

4.1. Code im Test

Erstellen wir eine Modellklasse mit dem Namen Item :

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

Wir müssen die Methode equals (Object other) überschreiben , damit unsere Behauptungen funktionieren. Spock verwendet bei Zusicherungen Gleichheit , wenn wir das doppelte Gleichheitszeichen (==) verwenden:

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

Erstellen wir nun eine Schnittstelle ItemProvider mit einer Methode:

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

Wir brauchen auch eine Klasse, die getestet wird. Wir werden einen ItemProvider als Abhängigkeit in ItemService hinzufügen:

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

Wir möchten, dass unser Code eher von einer Abstraktion als von einer bestimmten Implementierung abhängt. Deshalb verwenden wir eine Schnittstelle. Dies kann viele verschiedene Implementierungen haben. Beispielsweise könnten wir Elemente aus einer Datei lesen, einen HTTP-Client für einen externen Dienst erstellen oder die Daten aus einer Datenbank lesen.

In diesem Code müssen wir die externe Abhängigkeit stubben , da wir nur unsere Logik testen möchten, die in der Methode getAllItemsSortedByName enthalten ist .

4.2. Verwenden eines Stubbed-Objekts im zu testenden Code

Initialisieren wir das ItemService- Objekt in der setup () -Methode mithilfe eines Stubs für die ItemProvider- Abhängigkeit:

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

Nun lassen Sie uns machen ItemProvider mit dem spezifischen Argument bei jedem Aufruf eine Liste der Elemente zurückgeben :

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

Wir verwenden den Operanden >>, um die Methode zu stubben. Die Methode getItems gibt immer eine Liste mit zwei Elementen zurück, wenn sie mit der Liste ['Angebots-ID', 'Angebots-ID-2'] aufgerufen wird . [] ist eine Groovy- Verknüpfung zum Erstellen von Listen.

Hier ist die gesamte Testmethode:

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

Es gibt viel mehr Stubbing-Funktionen, die wir verwenden können, z. B.: Verwenden von Argumentabgleichsbeschränkungen, Verwenden von Wertesequenzen in Stubs, Definieren unterschiedlichen Verhaltens unter bestimmten Bedingungen und Verketten von Methodenantworten.

5. Verspottungsklassenmethoden

Lassen Sie uns nun über das Verspotten von Klassen oder Schnittstellen in Spock sprechen.

Manchmal möchten wir wissen, ob eine Methode des abhängigen Objekts mit angegebenen Argumenten aufgerufen wurde . Wir möchten uns auf das Verhalten der Objekte konzentrieren und untersuchen, wie sie interagieren, indem wir uns die Methodenaufrufe ansehen.Das Verspotten ist eine Beschreibung der obligatorischen Interaktion zwischen den Objekten in der Testklasse.

Wir werden die Interaktionen in dem unten beschriebenen Beispielcode testen.

5.1. Code mit Interaktion

Als einfaches Beispiel speichern wir Elemente in der Datenbank. Nach dem Erfolg möchten wir im Nachrichtenbroker ein Ereignis über neue Elemente in unserem System veröffentlichen.

Der Beispiel-Nachrichtenbroker ist ein RabbitMQ oder Kafka . Im Allgemeinen beschreiben wir nur unseren Vertrag:

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

Unsere Testmethode speichert nicht leere Elemente in der Datenbank und veröffentlicht dann das Ereignis. Das Speichern eines Elements in der Datenbank ist in unserem Beispiel irrelevant, daher geben wir nur einen Kommentar ein:

void saveItems(List itemIds) { List notEmptyOfferIds = itemIds.stream() .filter(itemId -> !itemId.isEmpty()) .collect(Collectors.toList()); // save in database notEmptyOfferIds.forEach(eventPublisher::publish); }

5.2. Überprüfen der Interaktion mit verspotteten Objekten

Lassen Sie uns nun die Interaktion in unserem Code testen.

Zuerst müssen wir EventPublisher in unserer setup () -Methode verspotten . Im Grunde genommen erstellen wir ein neues Instanzfeld und verspotten es mithilfe der Mock (Class) -Funktion:

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

Jetzt können wir unsere Testmethode schreiben. Wir werden 3 Strings übergeben: ”, 'a', 'b' und wir erwarten, dass unser eventPublisher 2 Events mit 'a' und 'b' Strings veröffentlicht:

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

Lassen Sie uns im Finale einen genaueren Blick auf unsere Behauptung nehmen dann Abschnitt:

1 * eventPublisher.publish('a')

Wir erwarten, dass itemService eine eventPublisher.publish (String) mit 'a' als Argument aufruft .

Beim Stubbing haben wir über Argumentationsbeschränkungen gesprochen. Gleiche Regeln gelten für Mocks. Wir können überprüfen, ob eventPublisher.publish (String) zweimal mit einem nicht null- und nicht leeren Argument aufgerufen wurde:

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

5.3. Mocking und Stubbing kombinieren

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 diesem Artikel haben wir Spione, Spott und Stummel in Groovy ausführlich beschrieben . Das Wissen zu diesem Thema macht unsere Tests schneller, zuverlässiger und leichter zu lesen.

Die Implementierung all unserer Beispiele finden Sie im Github-Projekt.