Integrationsbeauftragter
HTTP-APIs automatisch mit Postman testen
Je früher Softwareentwickler einen Fehler finden, desto weniger Arbeit und Kosten verursacht er. Der HTTP-Client Postman kann APIs automatisiert aufrufen und die Antworten systematisch prüfen – stimmen sie nicht mit den Erwartungen überein, hat sich ein Bug eingeschlichen.
Kurz vor Feierabend. Schnell noch ein winziges Problem am Code des HTTP-APIs gelöst, nur zwei kleine Zeilen Code. In Git eingecheckt, das Update auf den Server gebracht und das Notebook zugeklappt. Abends dann der Notruf: Die Kunden können sich nicht mehr einloggen, das API wirft an einer ganz anderen Stelle einen kryptischen Fehler. Es wird doch nicht? Es kann doch nicht? Die winzige Änderung darf doch nicht schuld sein. Oder?
HTTP-Schnittstellen finden sich in den meisten modernen Webanwendungen. Websites, Apps oder Sprachassistenten und andere Backend-Systeme kontaktieren sie. Diese Konsumenten können über die Schnittstelle Datensätze erstellen, lesen, aktualisieren oder löschen, häufig sind solche APIs nach dem REST-Schema aufgebaut [1]. Wenn sich in so ein API ein Fehler einschleicht, sieht der Nutzer schnell gar nichts mehr: API-Entwickler möchten diese Übeltäter also möglichst früh finden und beheben, damit es gar nicht erst zu Ausfällen kommt. Besonders bei Updates können Probleme an Stellen auftreten, an denen Entwickler sie gar nicht vermuten, weil sie den betreffenden Code vermeintlich nie angefasst haben.
Um unvorhergesehene Seiteneffekte vor dem Release zu entdecken, sollten Sie Ihre Software daher automatisiert testen. Auf der einen Seite geht das mit sogenannten Unit-Tests, die einzelne Funktionen innerhalb der Software auf korrektes Verhalten prüfen [2]. Solche Tests sind sinnvoll, können aber nicht immer garantieren, dass die Software in ihrer Gesamtheit auch wie geplant arbeitet. Bei APIs bieten sich daher zusätzlich Tests an, die einen Konsumenten imitieren und die Schnittstelle benutzen, wie es auch in der realen Welt passieren würde: sogenannte Integration-Tests.
Umgebung aufbauen
Werkzeuge für solche Tests von APIs gibt es verschiedene, eins ist der HTTP-Client Postman, den wir bereits in dieser Rolle ausführlich vorgestellt haben [3]. Mit der Oberfläche von Postman können Entwickler ihr API ausprobieren, mit verschiedenen Datensätzen befüllen oder fremde APIs erkunden – bequemer als mit einem Kommandozeilen-HTTP-Client wie Curl. Doch Postman bringt auch einen Werkzeugkasten für automatische Tests mit.
Damit Postman ein API testen kann, müssen Sie es über eine URL verfügbar machen. Wenn Sie lokal entwickeln, antwortet ein Entwicklungsserver in der Regel auf einem Port des internen Netzwerkadapters. Testkandidat für diesen Artikel ist ein JSON-API, das der Mock-Server JSON-Server bereitstellt. Dieses nützliche Open-Source-Projekt kann Datenobjekte aus einer JSON-Datei als REST-API anbieten (siehe ct.de/yzhk) und kann immer dann das Mittel zur Wahl sein, wenn das eigentliche API noch nicht fertig ist. Wie Sie mit JSON-Server ein API simulieren, haben wir bereits ausführlich vorgestellt [4].
Über ct.de/yzhk finden Sie eine Docker-Compose-Datei, mit der Sie einen Mock-Server auf Basis von JSON-Server starten. Als Beispiel soll das Test-API Konferenzräume in einem Bürogebäude verwalten. Legen Sie die Datei auf Ihrer Maschine ab und fahren den Container mit dem Befehl docker compose up hoch. Unter der Adresse http://localhost:3000 antwortet das Beispiel-API. Die Datei db.json enthält die Objekte, die über den JSON-Server angeboten werden: Räume und Kategorien. Die Endpunkte für Räume, die JSON-Server daraus zusammenbaut, sehen Sie in der Tabelle rechts.
| REST-Endpunkte eines Raumplan-APIs | |||
| Aktion | HTTP-Verb | Pfad | HTTP-Statuscode |
| Räume auflisten |
GET
|
/rooms
|
200 Ok
|
| Raum erstellen |
POST
|
/rooms
|
201 Created
|
| Raum abrufen |
GET
|
/rooms/:id
|
200 Ok
|
| Raum aktualisieren |
PUT
|
/rooms/:id
|
200 Ok
|
| Raum-Attribut aktualisieren |
PATCH
|
/rooms/:id
|
200 Ok
|
| Raum löschen |
DELETE
|
/rooms/:id
|
204 No Content
|
Collection erstellen
Jeden dieser Endpunkte soll Postman nun im Rahmen eines oder mehrerer Integration-Tests prüfen. Dafür müssen Sie im ersten Schritt eine Postman-Collection anlegen, die Ihre Requests für dieses API verwahrt. Ganz links in der Oberfläche gibt es dafür eine Schaltfläche mit einem Plus. Beginnen Sie mit dem Auflisten aller Räume: Wählen Sie dazu das Verb GET, geben Sie http://localhost:3000/rooms als URL an und testen den Request mit der Schaltfläche „Send“. Als Antwort sollten Sie nun die drei Räume angezeigt bekommen, die auch in der Datei db.json stehen.
Legen Sie die restlichen Endpunkte nach dem gleichen Muster als HTTP-Requests an und speichern Sie diese in der Collection. Anschließend können Sie links im Menü mit einem Rechtsklick auf den Namen der Collection klicken und dann „Run collection“ auswählen. Es öffnet sich ein Dialog, in dem Sie zunächst nichts ändern können. Hier könnten Sie zum Beispiel einzelne Aufrufe abwählen.
Klicken Sie unten auf den orangefarbenen Knopf, um den Testdurchlauf zu starten. Nach einigen Sekunden erhalten Sie einen Überblick über alle Aufrufe, Sie sehen die benötigte Zeit und die Statuscodes. Sie sehen aber auch, dass unter jedem Aufruf der Hinweis „No tests found“ steht. Postman hat zwar alle Endpunkte aufgerufen und festgestellt, dass ein Server irgendwie geantwortet hat, ob das so wie erwartet war, hat aber noch niemand geprüft – es wird Zeit, das zu ändern.
Tests schreiben
Versehen Sie zunächst den Aufruf GET /rooms, den ersten Endpunkt aus der Tabelle, mit einem Test. Klicken Sie dafür auf den Request und dort mittig auf den Tab „Tests“. Sie sehen nun einen Codeeditor, der JavaScript erwartet. Mit Code validieren Sie die Antwort. Um Ihnen den Einstieg zu erleichtern, zeigt Ihnen Postman im rechten Bereich bereits einige vorgeschlagene Schnipsel, unter anderem: „Status code: Code is 200“. Indem Sie dieses Snippet anklicken, taucht folgender Code im Editor auf:
pm.test("Status code is 200",function(){ pm.response.to.have.status(200); });
Mit der Funktion pm.test() legen Sie einen neuen Test an. Die Methode akzeptiert als erstes Argument einen sprechenden Namen, der später auch in der Übersicht aller Tests auftaucht. Das zweite Argument ist eine Funktion, in der die Testkriterien aufgelistet werden. Postman speist hier eine Vielzahl eigener Variablen wie pm.response ein, welche die Antwort vom Server enthält. Testen können Sie mit der Syntax des Test-Frameworks Chai (siehe ct.de/yzhk), mit dem Sie Datenstrukturen mit nahezu natürlicher Sprache auf bestimmte Eigenschaften abklopfen können – in diesem Fall auf den HTTP-Status des Aufrufs.
Mit ähnlicher Syntax können Sie auf den zurückgegebenen HTTP-Body zugreifen und diesen analysieren. Der folgende Code wandelt die Rückgabe in ein JSON-Objekt und prüft anschließend, ob alle drei erwarteten Räume vom Server zurückkommen:
pm.test("Collection has three items", function () { const data = pm.response.json(); pm.expect(data).to.have.lengthOf(3); });
Für jeden Test sollten Sie einen eigenen Block mit der Methode p.test() anlegen und einen sprechenden Namen vergeben. Theoretisch passen auch mehrere Tests in einen Block, aber dann wird die Fehlersuche unangenehm, weil nicht sofort klar ist, welcher Test fehlgeschlagen ist.
Nach dem gleichen Prinzip können Sie Ihre Rückgaben auf Herz und Nieren checken: Mit to.eql() können Sie auf einen erwarteten Wert testen, mit to.be.a() den Typ eines Attributs prüfen. Einen Überblick über alle sogenannten Assertions finden Sie in der Dokumentation (siehe ct.de/yzhk). Ein guter erster Test besteht immer darin, den zu erwartenden Statuscode für den Endpunkt zu prüfen und in weiteren Tests die konkreten Inhalte der Antwort.
Man kann aus dem JavaScript-Code heraus weitere HTTP-Aufrufe absetzen, um zum Beispiel Informationen oder Zugangs-Token zu beschaffen. Allerdings wird es schnell unübersichtlich, wann welcher Aufruf stattgefunden hat. Es empfiehlt sich daher, jeden Aufruf in Postman zu definieren und in der richtigen Reihenfolge in der Collection abzulegen.
Die Tests der einzelnen Endpunkte können dabei auch die Reise eines Nutzers durch eine Anwendung nacherzählen: Jemand legt einen Raum für den Raumplan an, zeigt sich alle Räume an, zeigt sich einen konkreten Raum an, löscht ihn und prüft dann, ob er auch wirklich verschwunden ist.
Variablen verwenden
Wenn Sie eine solche Reise nachbauen, kommen Sie mit statischen HTTP-Aufrufen nicht mehr weiter. Postman muss in die Lage versetzt werden, Informationen aus einem Aufruf im nächsten zu verwenden. Wenn Sie mit dem Verb POST am Endpunkt /rooms ein neues Objekt anlegen und es danach mit dem Verb GET am Endpunkt /rooms/:id abrufen wollen, stehen Sie vor dieser Herausforderung: Die ID des Objekts wird erst zur Laufzeit des Tests erzeugt. Um das Problem zu lösen, können Sie im Testcode auch Variablen aus der Collection auslesen und beschreiben. Diese Werte überdauern in einer Collection so lange, bis Sie sie löschen oder überschreiben – man kann Werte also aus einem Aufruf in einen anderen mitnehmen.
Der folgende Schnipsel entnimmt die ID aus dem Response-Body eines frisch angelegten Raums und schreibt sie in die Variable roomId:
const body = JSON.parse(responseBody); pm.collectionVariables.set( "roomId", body.id);
In allen Requests, die Sie danach ausführen werden, können Sie diese Variable dann in der URL mit doppelt geschweiften Klammern verwenden: /rooms/{{roomId}}.
Code vor HTTP
Manchmal ist es nötig, noch vor dem Ausführen eines Aufrufs einen Wert zu beschaffen oder zu generieren. Wenn Sie beispielsweise einen aktuellen Zeitstempel brauchen, um ihn im HTTP-Body eines neuen Objekts mitzuschicken, können Sie auf sogenannte „Pre-request Scripts“ zurückgreifen. Klicken Sie dafür auf den gleichnamigen Tab und fügen Sie Ihren Code in den Editor ein. In all diesen Skripten können Sie dafür auch auf alle Bibliotheken und JavaScript-Funktionen zurückgreifen, die Postman mitliefert: Moment.js erledigt Aufgaben rund um Zeit und Daten, btoa und atob hantiert mit Base64-Strings. Alle verfügbaren Bibliotheken und Funktionen finden Sie in der Dokumentation von Postman über ct.de/yzhk. Das folgende Beispiel befüllt die Collection-Variable tomorrow mithilfe von Moment.js mit dem Datum von morgen:
var moment = require('moment'); var tomorrow = moment().« »add(1, 'days').format(); pm.collectionVariables.set("tomorrow", tomorrow);
Darüber hinaus können Sie JavaScript-Code auch auf Ebene der gesamten Collection oder einzelner Ordner innerhalb der Collection definieren. Klicken Sie dafür auf die jeweilige Collection beziehungsweise den jeweiligen Ordner und wählen dort den Tab „Pre-request Scripts“ oder „Tests“ aus. Der dort hinterlegte Code wird dann vor oder nach jedem Request, der in dieser Collection oder diesem Ordner liegt, ausgeführt. Dabei wird erst der Code auf Collection-Ebene, dann der auf Ordnerebene und abschließend der Code am Request direkt ausgeführt. Code, den Sie nicht direkt an einem Request formulieren, eignet sich typisch, um Aufrufe zu debuggen, Sie können dort etwa mit console.log() den Zustand von Variablen loggen.
Tests ausführen
Wenn Sie alle Tests fertig geschrieben und eine kleine Reise durch die Anwendung formuliert haben, können Sie die Tests erneut starten. Haben Sie keine Fehler gemacht, dürften jetzt zahlreiche grüne Haken auftauchen. Diesen Durchlauf sollten Sie künftig nach jeder Änderung am Code in ihrem Projekt starten, um sicherzugehen, dass wirklich alles wie geplant funktioniert. Sie können sogar noch einen Schritt weiter gehen und auf dieser Grundlage Test-Driven-Deployment (TDD) einführen: Sobald Sie einen neuen Fehler entdecken, schreiben Sie in Postman einen Test, der genau diesen Fehler abprüft und der mit dem aktuellen Code fehlschlägt. Sie oder ein Kollege können sich jetzt an die Arbeit machen, eine Fehlerbehebung programmieren und wieder testen. Wenn wieder alle Tests erfolgreich durchlaufen, haben Sie Ihren Job erledigt. Und dank TDD können Sie sicher sein, dass dieser Fehler nie wieder ins fertige Produkt rutscht.
Fazit
Dass es Tests für Ihr API gibt und dass Postman sie der Reihe nach abarbeiten kann, ist die halbe Miete für schussfesten Code. Im letzten Schritt müssen Sie noch sichergehen, dass diese Tests auch wirklich jedes Mal ausgeführt werden, wenn es eine Änderung am Code gab. Das gelingt sehr elegant mit Continuous Integration/Continuous Delivery (CI/CD), also einer Automationsumgebung, die auf einer Versionsverwaltung (in der Regel Git) aufbaut und unter anderem solche Tests ausführt.
In einer perfekten Welt läuft eine Automation zum Beispiel an, wenn Sie einen Pull-Request anlegen. Sie baut Ihre Software, startet das API (zum Beispiel einem Container), startet im Falles eines APIs auch eine kleine Datenbank und startet dann eine Runde Postman-Integration-Tests. Für solche Fälle hat Postman noch einen Kollegen: Newman (siehe ct.de/yzhk), ein kommandozeilengestützter Runner für Postman-Collections. Denn in der CI/CD-Umgebung brauchen Sie keine grafische Postman-Oberfläche. Stattdessen generiert Newman am Ende einen Bericht. Wenn die Tests alle glatt durchlaufen, können Ihre Kollegen den Pull-Request guten Gewissens genehmigen, weil klar ist: Dieser Code macht genau, was sie von ihm erwarten. (jam@ct.de)
Dokumentation und Beispiele: ct.de/yzhk