Bei der Modernisierung oder gar Neuentwicklung bestehender Services kann vieles schief gehen: Da bereits Anwendungen implementiert worden sind, die sich auf die existierenden Schnittstellen verlassen, muss besonderes Augenmerk darauf liegen, dass ein REST-Endpunkt vor und nach der Modernisierung exakt die gleichen Daten im gleichen Format liefert.
Im Rahmen eines Kundenprojektes, in dem wir bestehende Microservices auf Spring Boot migriert und einen vom Kunden eigens entwickelten OR-Mapper durch Spring Data JPA ersetzt haben, bestand so nun die Anforderung, dass sich der Service nach der Migration/Modernisierung exakt wie zuvor zu verhalten habe. Der Kunde wünschte sich zudem Integrationstests in Form von SoapUI-Tests, da dieses Tool in einem vorigen Projekt bereits erfolgreich eingesetzt worden war. Bestehende Schnittstellentests/Contract Tests gab es nicht.
Glücklicherweise bietet SoapUI die Möglichkeit, per Groovy-Scripts dynamisch Einfluss auf das aktuelle Projekt zu nehmen. So waren wir in der Lage, Beispielaufrufe an die bestehenden Services abzusetzen, daraus einen "Snapshot" der Daten in Form von Assertions zu erstellen, und mit diesen Assertions schließlich den modernisierten Service zu testen, um Ungereimtheiten zu finden.
In diesem Artikel wollen wir einige der Konzepte im Groovy-Scripting für SoapUI beleuchten.
Hinweis SolcheSnapshotsvon Schnittstellen-Daten können natürlich auch in allgemeineren Szenarien nützlich sein; auch bei der Weiterentwicklung von Services kann es nützlich sein, zu verifizieren, dass nicht versehentlich andere Felder der Antworten verändert worden sind. FürveränderlicheDaten sind Snapshot-Vergleiche selbstverständlich nicht geeignet; sofern nur bestimmte Felder veränderlich sind, können diese über eine Blockliste ausgeschlossen werden. × Dismiss this alert.
Dieser Artikel präsentiert eine "abgespeckte" Version des fertigen Scripts, die als Grundlage für weitere Entwicklungen dienen kann. Keine Angst: Wer Erfahrung mit Java oder Kotlin hat, wird sich in Groovy schnell zurechtfinden und hat bereits die richtigen Instinkte.
Leider stellt es sich als etwas umständlich heraus, Code wiederverwertbar in SoapUI Test-Suiten zu hinterlegen. Glücklicherweise gibt es einen etwas ungewöhnlichen Weg, um Codeteile auszulagern und an anderer Stelle einzubinden.
Wir können somit die gemeinsame Logik für die Erstellung unserer Assertions (oder beliebigen anderen wiederverwendbaren Code) in einem zentralen Test Step anlegen. Als Beispiel legen wir einen Test Case namens groovy
an, der einen Groovy Test Step namens assertions
enthält. Den Test Case können wir dabei deaktivieren, da er keine tatsächlichen Tests enthalten wird und nur als Container dient. Der kompletten Quellcode kann am Ende dieses Artikels heruntergeladen werden; wir werden ihn im Laufe des Artikels aber auch gemeinsam entwickeln. Für den Start legen wir hier nur eine Klasse AssertionUtilities
an und hinterlegen eine Instanz dieser Klasse im context
-Objekt:
Hinweis Die JavaDocs zu SoapUI-Klassen sind leider recht unübersichtlich gestaltet, daher ist es häufig hilfreich, sich Details zu Objekten über die .class - eventuell unter Zuhilfenahme von Reflection - ausgeben zu lassen. Da der Code-Editor von SoapUI leider auch keine Unterstützung bietet, ist auch die Angabe von Typen wenig hilfreich (und in Groovy ohnehin optional). Ist der Typname bekannt, können die verfügbaren Methoden jedoch in der Regel über die Online-JavaDocs eingesehen werden, beispielsweise für TestCaseRunner. Im Rest des Artikels werden wir größtenteils darauf verzichten, Typen zu spezifizieren. × Dismiss this alert.
import org.apache.logging.log4j.core.Logger<br>import com.eviware.soapui.model.testsuite.TestCaseRunner<br>// Diese Imports werden wir später noch benötigen<br>import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequestStep<br>import com.eviware.soapui.impl.wsdl.teststeps.PropertyTransfersTestStep<br>import com.eviware.soapui.impl.wsdl.teststeps.JdbcRequestTestStep<br>import groovy.json.JsonSlurper<br>import groovy.json.JsonException<br>class AssertionUtilities {<br> Logger log<br> Object context<br> TestCaseRunner testRunner<br> JsonSlurper jsonSlurper<br> def AssertionUtilities(Logger log, Object context, TestCaseRunner testRunner) {<br> this.log = log<br> this.context = context<br> this.testRunner = testRunner<br> this.jsonSlurper = new JsonSlurper()<br> }<br> def createAssertionsForTestCase() {<br> log.info("dummy")<br> }<br>}<br>context.assertions = new AssertionUtilities(log, context, testRunner)
Die tatsächlichen Testfälle können anschließend ebenfalls als Test Cases und Test Steps angelegt werden. Jeder Test Case wird dann einen zusätzlichen (wiederum deaktivierten) Groovy Test Step enthalten, der dafür zuständig ist, die gewünschten Assertions zu generieren. Diese Steps sehen alle sehr ähnlich aus: Sie importieren zunächst den gemeinsam genutzten Code und rufen anschließend nur die Funktion createAssertionsForTestCase
auf, der wir in späteren Artikeln noch Parameter geben werden, die die genaue Ausgestaltung der Assertions kontrollieren.
Es ist wichtig, den jeweiligen Test Step zu deaktivieren, damit er tatsächlich nur manuell ausgeführt werden kann; ansonsten würden die Assertions der anderen Test Steps bei jedem Ausführen der Test Cases neu generiert - im besten Falle kostet dies lediglich etwas Zeit, im schlechtesten Falle können dadurch jedoch Fehler unentdeckt bleiben. Deaktivierte Test Steps werden bei der Ausführung eines kompletten Test Case oder sogar der kompletten Test Suite ignoriert, lassen sich aber über den grünen Pfeil im Script-Editor regulär ausführen.
// Library importieren, via https://blog.sysco.no/testing/scriptlibrary-in-soapui/<br>if(context.assertions == null) {<br> testRunner.testCase.testSuite<br> .getTestCaseByName("groovy")<br> .getTestStepByName("assertions")<br> .run(testRunner, context)<br>}<br>context.assertions.createAssertionsForTestCase()
Die finale Struktur unseres Testprojektes sollte nun in etwa so aussehen; zu beachten sind insbesondere der deaktivierte Testfall groovy
sowie der deaktivierte Testschritt Assertions generieren
.
Zum Start ein kurzer Refresher zur Terminologie: Schnittstellen-Tests bilden in SoapUI eine Baumstruktur bestehend aus folgenden Ebenen:
Der Test Case kann dabei theoretisch alles mögliche sein: Ein fachlicher Testfall (etwa ein “Passwort zurücksetzen”-Flow) mit mehreren aufeinander aufbauenden Schritten ist ebenso denkbar wie eine Liste nicht zusammenhängender Tests mit unterschiedlichen Eingabedaten. Ein Test Case kann aus einer beliebigen Kombination von Test Steps bestehen, beispielsweise könnte ein JDBC Request zunächst einen definierten Zustand in einer Datenbank herstellen; ein REST Request führt dann eine Aktion aus, ein Property Transfer überträgt einen Teil der Antwort (etwa eine ID) in einen zweiten REST Request und zum Abschluss wird per JDBC Request wieder der Ausgangszustand wiederhergestellt.
Die Aufgabe unserer Funktion createAssertionsForTestCase
wird es zunächst sein, alle Steps des übergeordneten Test Case des Groovy-Scripts zu durchlaufen. Wir können an dieser Stelle auf unterschiedliche Arten von Test Steps reagieren, beispielsweise könnten wir eine gesonderte Behandlung von JDBC Request- und Property Transfer-Steps implementieren oder die Generierung von Assertions für REST Request-Steps gegen bestimmte Services (beispielsweise einen Login-Service) unterdrücken. In unserem Fall ist die Logik vergleichsweise einfach: Ist ein Test Step deaktiviert, wird er übersprungen, ansonsten wird er ausgeführt.
Der TestCaseRunner
, den wir im Konstruktor unserer AssertionUtilities
-Klasse übergeben haben (und der wiederum vom jeweils ausgeführten Test Case befüllt wird), bietet uns bereits Zugriff auf den aktuellen TestCase
(über die testCase
-Property). Auf dessen Kind-Elemente (die einzelnen Testschritte für einen Testfall) können wir wiederum per testStepList
-Property zugreifen.
Auch einen Testschritt auszuführen ist denkbar einfach: Wir rufen die run
-Methode des Schritts auf und übergeben unseren TestCaseRunner
und den context
.
Zum Abschluss prüfen wir, ob es sich beim aktuellen Testschritt um einen RestTestRequestStep
handelt: Nur dann werden anschließend auch Assertions generiert.
def createAssertionsForTestCase() {<br> for(step in testRunner.testCase.testStepList) {<br> if(step.disabled) {<br> continue<br> }<br> step.run(testRunner, context)<br> // Nur für Rest Request Steps wollen wir Assertions generieren<br> if(step instanceof RestTestRequestStep) {<br> def response = step.httpRequest.response<br> log.info("[${step.label}] Assertions werden erzeugt...")<br> createHttpStatusAssertion(step, response.statusCode)<br> createContentAssertions(step, response.responseContent)<br> log.info("[${step.label}] Assertions erfolgreich erzeugt.")<br> }<br> }<br>}<br>def createContentAssertions(step, responseContent) {<br> try {<br> def data = jsonSlurper.parseText(responseContent)<br> generateAssertionsForValue(step, "$", data)<br> } catch(JsonException e) {<br> testRunner.fail("${step.label} hat kein JSON geliefert; fehlt ein Accept-Header?")<br> log.info(e.message)<br> }<br>}
Einem Test Step Assertions hinzuzufügen passiert über die Methode addAssertion
, die gleichzeitig auch als Factory dient: Sie gibt die zu verwendende Instanz der Assertion zurück, die dann konfiguriert werden kann. Ein String-Parameter, der dem Namen der Assertion in der Benutzeroberfläche entspricht (etwa “Valid HTTP Status Codes”) gibt dabei an, welche Art von Assertion erzeugt werden soll. Diese API ist zugegebenermaßen gewöhnungsbedürftig, den HTTP-Statuscode zu validieren fällt damit dennoch einfach:
def createHttpStatusAssertion(step, statusCode) {<br> def name = ":status = $statusCode"<br> // Die Namen von Assertions müssen innerhalb eines Testschritts<br> // immer eindeutig sein; zudem wollen wir verhindern, dass<br> // mehrfache Aufrufe unseres Scripts (etwa, nachdem neue Felder<br> // hinzugefügt wurden) Duplikate bestehender Assertions anlegen.<br> if(step.getAssertionByName(name) == null) {<br> log.debug("Erstelle $name")<br> def assertion = step.addAssertion("Valid HTTP Status Codes")<br> assertion.name = name<br> assertion.codes = statusCode<br> } else {<br> log.info("Überspringe $name, da sie bereits existiert")<br> }<br>}
Die Validierung unserer eigentlichen Daten ist allerdings etwas komplizierter. Es wäre natürlich trivial, den Textinhalt der Antwort 1:1 zu vergleichen, jedoch würde hier bereits eine Abweichung in der Formatierung oder der Reihenfolge von Feldern - Dinge, die auf die konsumierenden Services keinen Einfluss haben sollten - dazu führen, dass die Ergebnisse nicht mehr gleich sind. Zudem wäre eine Fehlermeldung im Sinne von "irgendwo im JSON gibt es eine Abweichung" nicht sehr hilfreich. An dieser Stelle können wir uns mit JsonPath Match Assertions behelfen: Sie prüfen einen Wert an einer bestimmten Stelle des JSON-Dokuments auf einen vorgegebenen Wert. In unserem Falle werden wir rekursiv die gesamte Antwort durchlaufen und für jedes Feld eine Assertion generieren.
Hinweis Hier ist Vorsicht geboten, da viele Assertions SoapUI schnell in die Knie zwingen können - die Erzeugung mehrerer tausend Assertions kann schnell mehrere Minuten in Anspruch nehmen. Zudem wächst mit jeder Assertion natürlich auch die SoapUI-Projektdatei. × Dismiss this alert.
Um nun alle Felder unserer Antwort zu validieren, müssen wir zunächst durch die Antwort iterieren: Für jeden Wert müssen wir zwischen einem JSON-Objekt, einem JSON-Array und "nativen" Werten (Strings, Booleans, Numbers, null) unterscheiden. Kommen wir bei einem "nativen" Wert an, generieren wir einen JsonPath und legen eine Assertion an, um den entsprechenden Wert zu überprüfen.
Zunächst prüfen wir den konkreten Typen des aktuellen Werts. Da wir mit einem untypisierten Modell der Daten arbeiten, gibt uns der JsonSlurper für komplexe Werte (Arrays und Objekte) Instanzen von List
und Map
zurück. Wir durchlaufen in beiden Fällen die einzelnen Elemente und erzeugen einen “laufenden” JsonPath-Pfad mithilfe der Indexer-Syntax ($.liste[0]
für Arrays, $.objekt['key']
für Objekte). Für alle anderen Werte erzeugen wir dann tatsächlich eine JsonPath Match-Assertion mit dem bis dorthin gefundenen Pfad.
Über JsonPath JsonPathdefiniert den Zugriff auf ein Teildokument in einem größeren JSON-Dokument. Der oben verlinkte Artikel von Stefan Gössner gibt einen guten Eindruck von der Syntax von JsonPath. Pfade in JsonPath-Notation sind in der Regel recht selbsterklärend,$.employees[0].namebeschreibt etwa den "Pfad" zum Namen des ersten Mitarbeiters in einem JSON-Dokument mit folgendem Aufbau:{"employees": [ { "id": 123, "name": "Max Mustermann" } ] } × Dismiss this alert.
def generateAssertionsForValue(step, path, value) {<br> if(value instanceof List) {<br> return generateAssertionsForList(step, path, value)<br> } else if(value instanceof Map) {<br> return generateAssertionsForMap(step, path, value)<br> } else {<br> return createJsonPathContentAssertion(step, path, value)<br> }<br>}<br>def generateAssertionsForList(step, path, list) {<br> list.eachWithIndex { entry, i -><br> generateAssertionsForValue(step, "$path[$i]", entry)<br> }<br>}<br>def generateAssertionsForMap(step, path, map) {<br> for(entry in map) {<br> generateAssertionsForValue(step, "$path['${entry.key}']", entry.value)<br> }<br>}<br>def createJsonPathContentAssertion(step, path, value) {<br> def name = "$path = $value"<br> if(step.getAssertionByName(name) == null) {<br> log.debug("Erstelle $name")<br> def assertion = step.addAssertion("JsonPath Match")<br> assertion.name = name<br> assertion.path = path<br> assertion.expectedContent = value != null ? value : "null"<br> } else {<br> log.info("Überspringe $name, da sie bereits existiert")<br> }<br>}
An dieser Stelle haben wir bereits ein funktionierendes Framework, um unsere Assertions komplett automatisiert zu erzeugen.
Obwohl die API von SoapUI mitunter gewöhnungsbedürftig ist, waren wir dank Groovy in der Lage, mit überschaubarem Aufwand eine solide Grundlage aufzubauen und haben die Grundlagen des Scriptings mit Groovy kennengelernt.
Im weiteren Verlauf kann es nun hilfreich sein, folgende Features umzusetzen:
Diese Features sind mit vergleichbar geringem Aufwand umsetzbar, würden im Rahmen dieses Artikels allerdings zu sehr vom Kern ablenken. Wir werden sie daher in einem späteren Artikel näher beleuchten.
Zum Abschluss können hier ein Testprojekt sowie der Quellcode zur Erzeugung der Assertions in der in diesem Artikel vorgestellten Fassung heruntergeladen werden:
Code herunterladen (.groovy) Beispielprojekt herunterladen (.xml)