paint-brush
Erstellen effektiver Integrationstests: Best Practices und Tools im Spring Frameworkvon@avvero
531 Lesungen
531 Lesungen

Erstellen effektiver Integrationstests: Best Practices und Tools im Spring Framework

von Anton Belyaev8m2024/05/26
Read on Terminal Reader

Zu lang; Lesen

Dieser Artikel bietet praktische Empfehlungen zum Schreiben von Integrationstests und zeigt, wie Sie sich auf die Spezifikationen der Interaktionen mit externen Diensten konzentrieren können, um die Tests lesbarer und einfacher zu warten zu machen. Der Ansatz verbessert nicht nur die Effizienz des Testens, sondern fördert auch ein besseres Verständnis der Integrationsprozesse innerhalb der Anwendung. Anhand spezifischer Beispiele werden verschiedene Strategien und Tools – wie DSL-Wrapper, JsonAssert und Pact – untersucht, um dem Leser einen umfassenden Leitfaden zur Verbesserung der Qualität und Sichtbarkeit von Integrationstests zu bieten.
featured image - Erstellen effektiver Integrationstests: Best Practices und Tools im Spring Framework
Anton Belyaev HackerNoon profile picture
0-item

In der modernen Softwareentwicklung spielt effektives Testen eine Schlüsselrolle, um die Zuverlässigkeit und Stabilität von Anwendungen sicherzustellen.


Dieser Artikel bietet praktische Empfehlungen zum Schreiben von Integrationstests und zeigt, wie Sie sich auf die Spezifikationen der Interaktionen mit externen Diensten konzentrieren können, um die Tests lesbarer und einfacher zu warten zu machen. Der Ansatz verbessert nicht nur die Effizienz des Testens, sondern fördert auch ein besseres Verständnis der Integrationsprozesse innerhalb der Anwendung. Anhand spezifischer Beispiele werden verschiedene Strategien und Tools – wie DSL-Wrapper, JsonAssert und Pact – untersucht, um dem Leser einen umfassenden Leitfaden zur Verbesserung der Qualität und Sichtbarkeit von Integrationstests zu bieten.


Der Artikel präsentiert Beispiele für Integrationstests, die mit dem Spock Framework in Groovy durchgeführt wurden, um HTTP-Interaktionen in Spring-Anwendungen zu testen. Gleichzeitig können die vorgeschlagenen Haupttechniken und Ansätze effektiv auf verschiedene Arten von Interaktionen über HTTP hinaus angewendet werden.

Problembeschreibung

Der Artikel „Schreiben effektiver Integrationstests in Spring: Organisierte Teststrategien für HTTP-Anforderungs-Mocking“ beschreibt einen Ansatz zum Schreiben von Tests mit einer klaren Trennung in unterschiedliche Phasen, von denen jede ihre spezifische Rolle erfüllt. Lassen Sie uns ein Testbeispiel gemäß diesen Empfehlungen beschreiben, aber mit dem Mocking nicht einer, sondern zweier Anforderungen. Die Act-Phase (Ausführung) wird der Kürze halber weggelassen (ein vollständiges Testbeispiel finden Sie im Projekt-Repository ).

Der dargestellte Code ist bedingt in die Teile „Unterstützender Code“ (grau markiert) und „Spezifikation externer Interaktionen“ (blau markiert) unterteilt. Der unterstützende Code enthält Mechanismen und Dienstprogramme zum Testen, einschließlich des Abfangens von Anfragen und Emulierens von Antworten. Die Spezifikation externer Interaktionen beschreibt spezifische Daten zu externen Diensten, mit denen das System während des Tests interagieren soll, einschließlich erwarteter Anfragen und Antworten. Der unterstützende Code legt die Grundlage für das Testen, während sich die Spezifikation direkt auf die Geschäftslogik und die Hauptfunktionen des Systems bezieht, das wir testen möchten.


Die Spezifikation nimmt einen kleinen Teil des Codes ein, ist aber für das Verständnis des Tests von erheblichem Wert, während der unterstützende Code, der einen größeren Teil einnimmt, weniger wertvoll ist und sich für jede Mock-Deklaration wiederholt. Der Code ist für die Verwendung mit MockRestServiceServer vorgesehen. Wenn man sich das Beispiel auf WireMock ansieht, erkennt man dasselbe Muster: Die Spezifikation ist fast identisch und der unterstützende Code variiert.


Das Ziel dieses Artikels besteht darin, praktische Empfehlungen zum Schreiben von Tests zu geben, sodass der Fokus auf der Spezifikation liegt und der unterstützende Code in den Hintergrund tritt.

Demonstrationsszenario

Für unser Testszenario schlage ich einen hypothetischen Telegram-Bot vor, der Anfragen an die OpenAI-API weiterleitet und Antworten an die Benutzer zurücksendet.

Die Verträge für die Interaktion mit Diensten werden vereinfacht beschrieben, um die Hauptlogik des Vorgangs hervorzuheben. Unten sehen Sie ein Sequenzdiagramm, das die Anwendungsarchitektur veranschaulicht. Ich verstehe, dass das Design aus Sicht der Systemarchitektur Fragen aufwerfen könnte, aber gehen Sie bitte mit Verständnis an die Sache heran – das Hauptziel besteht hier darin, einen Ansatz zur Verbesserung der Sichtbarkeit in Tests zu demonstrieren.

Vorschlag

In diesem Artikel werden die folgenden praktischen Empfehlungen zum Schreiben von Tests erläutert:

  • Verwendung von DSL-Wrappern für die Arbeit mit Mocks.
  • Verwendung von JsonAssert zur Ergebnisüberprüfung.
  • Speichern der Spezifikationen externer Interaktionen in JSON-Dateien.
  • Verwendung von Pact-Dateien.

Verwenden von DSL-Wrappern zum Mocking

Die Verwendung eines DSL-Wrappers ermöglicht das Ausblenden des Mock-Codes im Boilerplate-Format und bietet eine einfache Schnittstelle zum Arbeiten mit der Spezifikation. Es ist wichtig zu betonen, dass es sich hier nicht um eine spezifische DSL handelt, sondern um einen allgemeinen Ansatz, den sie implementiert. Ein korrigiertes Testbeispiel mit DSL wird unten dargestellt ( vollständiger Testtext ).

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess("{...}")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Wobei die Methode restExpectation.openai.completions beispielsweise wie folgt beschrieben wird:

 public interface OpenaiMock { /** * This method configures the mock request to the following URL: {@code https://api.openai.com/v1/chat/completions} */ RequestCaptor completions(DefaultResponseCreator responseCreator); }

Wenn Sie einen Kommentar zur Methode haben, können Sie beim Überfahren des Methodennamens im Code-Editor mit der Maus Hilfe erhalten und auch die URL anzeigen, die simuliert wird.

In der vorgeschlagenen Implementierung erfolgt die Deklaration der Antwort aus dem Mock mithilfe von ResponseCreator -Instanzen, wobei auch benutzerdefinierte Instanzen möglich sind, wie beispielsweise:

 public static ResponseCreator withResourceAccessException() { return (request) -> { throw new ResourceAccessException("Error"); }; }

Unten sehen Sie einen Beispieltest für erfolglose Szenarios, bei dem eine Reihe von Antworten angegeben werden:

 import static org.springframework.http.HttpStatus.FORBIDDEN setup: def openaiRequestCaptor = restExpectation.openai.completions(openaiResponse) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 0 where: openaiResponse | _ withResourceAccessException() | _ withStatus(FORBIDDEN) | _

Bei WireMock ist alles gleich, außer dass die Antwortbildung etwas anders ist ( Testcode , Code der Antwortfabrikklasse ).

Verwenden der Annotation @Language("JSON") für eine bessere IDE-Integration

Bei der Implementierung einer DSL ist es möglich, Methodenparameter mit @Language("JSON") zu kommentieren, um die Sprachfunktionsunterstützung für bestimmte Codeausschnitte in IntelliJ IDEA zu aktivieren. Mit JSON behandelt der Editor beispielsweise den String-Parameter als JSON-Code und aktiviert Funktionen wie Syntaxhervorhebung, Autovervollständigung, Fehlerprüfung, Navigation und Struktursuche. Hier ist ein Beispiel für die Verwendung der Anmerkung:

 public static DefaultResponseCreator withSuccess(@Language("JSON") String body) { return MockRestResponseCreators.withSuccess(body, APPLICATION_JSON); }

So sieht es im Editor aus:

Verwenden von JsonAssert zur Ergebnisüberprüfung

Die JSONAssert-Bibliothek wurde entwickelt, um das Testen von JSON-Strukturen zu vereinfachen. Sie ermöglicht Entwicklern einen einfachen Vergleich erwarteter und tatsächlicher JSON-Zeichenfolgen mit einem hohen Maß an Flexibilität und unterstützt verschiedene Vergleichsmodi.

Dies ermöglicht den Übergang von einer Überprüfungsbeschreibung wie dieser

 openaiRequestCaptor.body.model == "gpt-3.5-turbo" openaiRequestCaptor.body.messages.size() == 1 openaiRequestCaptor.body.messages[0].role == "user" openaiRequestCaptor.body.messages[0].content == "Hello!"

zu so etwas

 assertEquals("""{ "model": "gpt-3.5-turbo", "messages": [{ "role": "user", "content": "Hello!" }] }""", openaiRequestCaptor.bodyString, false)

Meiner Meinung nach besteht der Hauptvorteil des zweiten Ansatzes darin, dass er die Konsistenz der Datendarstellung in verschiedenen Kontexten gewährleistet – in Dokumentation, Protokollen und Tests. Dies vereinfacht den Testprozess erheblich und bietet Flexibilität beim Vergleich und Genauigkeit bei der Fehlerdiagnose. So sparen wir nicht nur Zeit beim Schreiben und Warten von Tests, sondern verbessern auch deren Lesbarkeit und Aussagekraft.

Wenn Sie in Spring Boot arbeiten, sind ab Version 2 keine zusätzlichen Abhängigkeiten erforderlich, um mit der Bibliothek zu arbeiten, da org.springframework.boot:spring-boot-starter-test bereits eine Abhängigkeit von org.skyscreamer:jsonassert enthält.

Speichern der Spezifikation externer Interaktionen in JSON-Dateien

Eine Beobachtung, die wir machen können, ist, dass JSON-Strings einen erheblichen Teil des Tests einnehmen. Sollten sie ausgeblendet werden? Ja und nein. Es ist wichtig zu verstehen, was mehr Vorteile bringt. Wenn sie ausgeblendet werden, werden die Tests kompakter und das Wesentliche des Tests wird auf den ersten Blick leichter zu erfassen sein. Andererseits wird für eine gründliche Analyse ein Teil der entscheidenden Informationen über die Spezifikation der externen Interaktion ausgeblendet, was zusätzliche Sprünge zwischen Dateien erfordert. Die Entscheidung hängt von der Bequemlichkeit ab: Tun Sie, was für Sie bequemer ist.

Wenn Sie JSON-Strings in Dateien speichern möchten, besteht eine einfache Möglichkeit darin, Antworten und Anfragen getrennt in JSON-Dateien zu speichern. Unten sehen Sie einen Testcode ( Vollversion ), der eine Implementierungsoption demonstriert:

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json"))) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Die Methode fromFile liest einfach einen String aus einer Datei im Verzeichnis src/test/resources und beinhaltet keine revolutionäre Idee, ist aber dennoch im Projekt-Repository als Referenz verfügbar.

Für den variablen Teil der Zeichenfolge wird empfohlen, die Ersetzung mit org.apache.commons.text.StringSubstitutor zu verwenden und bei der Beschreibung des Mocks einen Wertesatz zu übergeben, zum Beispiel:

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json", [content: "Hello! How can I assist you today?"])))

Dabei sieht der Teil mit der Ersetzung in der JSON-Datei folgendermaßen aus:

 ... "message": { "role": "assistant", "content": "${content:-Hello there, how may I assist you today?}" }, ...

Die einzige Herausforderung für Entwickler bei der Einführung des Dateispeicheransatzes besteht darin, ein geeignetes Dateiplatzierungsschema in Testressourcen und ein Benennungsschema zu entwickeln. Dabei können leicht Fehler passieren, die die Arbeit mit diesen Dateien verschlechtern können. Eine Lösung für dieses Problem könnte die Verwendung von Spezifikationen wie denen von Pact sein, die später erläutert werden.

Wenn Sie den beschriebenen Ansatz in Tests verwenden, die in Groovy geschrieben sind, kann es zu Unannehmlichkeiten kommen: In IntelliJ IDEA gibt es keine Unterstützung für die Navigation zur Datei vom Code aus, aber die Unterstützung für diese Funktion wird voraussichtlich in Zukunft hinzugefügt . In Tests, die in Java geschrieben sind, funktioniert dies hervorragend.

Verwenden von Pact-Vertragsdateien

Beginnen wir mit der Terminologie.


Vertragstests sind eine Methode zum Testen von Integrationspunkten, bei der jede Anwendung isoliert getestet wird, um zu bestätigen, dass die von ihr gesendeten oder empfangenen Nachrichten einem in einem „Vertrag“ dokumentierten gegenseitigen Verständnis entsprechen. Dieser Ansatz stellt sicher, dass die Interaktionen zwischen verschiedenen Teilen des Systems den Erwartungen entsprechen.


Ein Vertrag im Rahmen von Vertragstests ist ein Dokument oder eine Spezifikation, die eine Vereinbarung über das Format und die Struktur von Nachrichten (Anfragen und Antworten) festhält, die zwischen Anwendungen ausgetauscht werden. Er dient als Grundlage für die Überprüfung, ob jede Anwendung die von anderen in der Integration gesendeten und empfangenen Daten korrekt verarbeiten kann.


Der Vertrag wird zwischen einem Verbraucher (z. B. einem Client, der Daten abrufen möchte) und einem Anbieter (z. B. einer API auf einem Server, der die vom Client benötigten Daten bereitstellt) geschlossen.


Verbrauchergesteuertes Testen ist ein Ansatz zum Testen von Verträgen, bei dem Verbraucher während ihrer automatisierten Testläufe Verträge generieren. Diese Verträge werden an den Anbieter weitergeleitet, der dann seine Reihe automatisierter Tests ausführt. Jede in der Vertragsdatei enthaltene Anfrage wird an den Anbieter gesendet und die empfangene Antwort wird mit der in der Vertragsdatei angegebenen erwarteten Antwort verglichen. Wenn beide Antworten übereinstimmen, bedeutet dies, dass Verbraucher und Dienstanbieter kompatibel sind.


Und schließlich Pact. Pact ist ein Tool, das die Ideen des verbrauchergesteuerten Vertragstests umsetzt. Es unterstützt das Testen sowohl von HTTP-Integrationen als auch von nachrichtenbasierten Integrationen und konzentriert sich auf die Code-First-Testentwicklung.

Wie ich bereits erwähnt habe, können wir für unsere Aufgabe die Vertragsspezifikationen und Tools von Pact verwenden. Die Implementierung könnte folgendermaßen aussehen ( vollständiger Testcode ):

 setup: def openaiRequestCaptor = restExpectation.openai.completions(fromContract("openai/SuccessfulCompletion-Hello.json")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Die Vertragsakte steht zur Einsichtnahme bereit .

Der Vorteil der Verwendung von Vertragsdateien besteht darin, dass sie nicht nur den Anforderungs- und Antworttext, sondern auch andere Elemente der Spezifikation für externe Interaktionen enthalten – Anforderungspfad, Header und HTTP-Antwortstatus –, sodass ein Mock basierend auf einem solchen Vertrag vollständig beschrieben werden kann.

Es ist wichtig zu beachten, dass wir uns in diesem Fall auf Vertragstests beschränken und nicht auf verbrauchergesteuerte Tests eingehen. Es kann jedoch sein, dass jemand Pact näher erkunden möchte.

Abschluss

In diesem Artikel wurden praktische Empfehlungen zur Verbesserung der Sichtbarkeit und Effizienz von Integrationstests im Rahmen der Entwicklung mit dem Spring Framework besprochen. Mein Ziel war es, die Bedeutung einer klaren Definition der Spezifikationen externer Interaktionen und der Minimierung von Boilerplate-Code hervorzuheben. Um dieses Ziel zu erreichen, habe ich vorgeschlagen, DSL-Wrapper und JsonAssert zu verwenden, Spezifikationen in JSON-Dateien zu speichern und mit Verträgen über Pact zu arbeiten. Die im Artikel beschriebenen Ansätze zielen darauf ab, den Prozess des Schreibens und Wartens von Tests zu vereinfachen, ihre Lesbarkeit zu verbessern und vor allem die Qualität des Tests selbst zu steigern, indem die Interaktionen zwischen Systemkomponenten genau widergespiegelt werden.


Link zum Projekt-Repository, das die Tests demonstriert – Sandbox/Bot .


Vielen Dank für Ihre Aufmerksamkeit beim Lesen des Artikels und viel Erfolg bei Ihrem Vorhaben, wirksame und sichtbare Tests zu verfassen!