Eine Ressource von Anfang bis Ende testen
Anlegen, lesen, ändern und löschen, und prüfen, dass jede Operation wirklich passiert ist.
Die meisten APIs sind um Ressourcen herum gebaut: Bücher, Bestellungen, Nutzer, Rechnungen. Für jede Ressource gibt es meist einen Weg, sie anzulegen, zu lesen, zu ändern und zu löschen. Beim Testen heißt das CRUD (create, read, update, delete), und ein CRUD-Test begleitet einen Datensatz durch sein ganzes Leben. Das ist das Erste, was du bei einer neuen Ressource testen solltest, weil fast jede andere Funktion davon abhängt.
Diese Lektion nutzt /api/Booksanmelden zum Anzeigen. Es unterstützt jede Operation, und seine Referenzseite listet die Felder auf. Die Methoden kennst du schon aus HTTP-Methoden. Hier lautet die Frage anders: Wie beweist du, dass jede Operation getan hat, was sie behauptet?
Eine Antwort ist eine Behauptung, kein Beweis
Wenn ein Server ein POST mit 201 und einer Kopie des Datensatzes beantwortet, sagt er dir, was er glaubt, gespeichert zu haben. Diese Kopie wird meist aus demselben Objekt gebaut, das gerade gespeichert werden sollte, sie kann also perfekt aussehen, selbst wenn das Speichern gescheitert ist oder ein Feld auf dem Weg zur Datenbank verloren ging. Was wirklich gespeichert wurde, erfährst du nur, wenn du in einem eigenen Request erneut danach fragst.
Ein CRUD-Test wechselt also zwischen etwas tun und es mit einem frischen Lesen prüfen:
- Leg einen Datensatz an. Prüf die Antwort.
- Lies ihn per ID zurück. Vergleich jedes Feld mit dem, was du gesendet hast.
- Finde ihn im Listen-Endpunkt.
- Ersetz ihn mit PUT. Lies ihn zurück.
- Ändere ein Feld mit PATCH. Lies ihn zurück.
- Lösch ihn. Lies ihn zurück und erwarte ein 404.
- Lösch ihn noch einmal und sieh, was passiert.
Anlegen
curl -i -X POST https://api.testingapis.com/api/Books \
-H "Content-Type: application/json" \
-d '{
"title": "Testing Web APIs",
"author": "Mark Winteringham",
"isbn": "9781617299537",
"genre": "testing",
"publishedYear": 2022,
"pages": 264,
"language": "English",
"price": 49.99,
"rating": 4.6,
"available": true,
"tags": ["api", "testing"]
}'HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{
"message": "Book created successfully",
"data": {
"id": 26,
"title": "Testing Web APIs",
"author": "Mark Winteringham",
"isbn": "9781617299537",
"genre": "testing",
"publishedYear": 2022,
"pages": 264,
"language": "English",
"price": 49.99,
"rating": 4.6,
"available": true,
"tags": ["api", "testing"]
}
}Was du bei dieser Antwort prüfst, grob nach Wichtigkeit geordnet:
- Der Status ist 201, nicht 200. Beides bedeutet Erfolg, aber 201 sagt, dass eine neue Ressource existiert, und Clients verzweigen manchmal danach.
- Es gibt eine
id, und sie ist eine Zahl (oder welcher Typ auch immer dokumentiert ist). Merk sie dir. Jeder spätere Schritt braucht sie. - Jedes gesendete Feld kommt mit demselben Wert zurück. Vergleich sie einzeln, auch die langweilig wirkenden. Ein Preis von
49.99, der als49.9899999oder"49.99"zurückkommt, ist ein Bug. - Ob ein
Location-Header auf den neuen Datensatz zeigt. Viele REST-APIs schicken bei einem 201 einen mit. Diese nicht. Das ist eher eine Designentscheidung als ein Fehler, aber du solltest wissen, wie deine API es hält, und es in deine Tests schreiben.
Sieh dir jetzt an, was bei einem kleinen Fehler im Body passiert. Das Feld heißt publishedYear, und hier wird es als year gesendet:
curl -X POST https://api.testingapis.com/api/Books \
-H "Content-Type: application/json" \
-d '{"title": "The Test Book", "author": "Ada Tester", "year": 2026}'{
"message": "Book created successfully",
"data": {
"id": 26,
"title": "The Test Book",
"author": "Ada Tester",
"isbn": "",
"genre": "",
"publishedYear": 0,
"pages": 0,
"language": "",
"price": 0,
"rating": 0,
"available": false,
"tags": []
}
}Der Server hat den Request angenommen, year kommentarlos ignoriert und 0als Jahr gespeichert. Nichts im Statuscode verrät, dass etwas schiefgelaufen ist. Das findest du nur, wenn du den gespeicherten Datensatz mit dem vergleichst, was du senden wolltest. Stillschweigend ignorierte Felder sind eine der häufigsten Quellen für Bugs der Sorte "die Daten sind falsch, und keiner weiß warum". Manche APIs lehnen unbekannte Felder stattdessen ab; die nächste Lektion behandelt das.
Felder, die der Server ausfüllt
Manche Felder kommen nicht von dir: die ID und bei vielen Ressourcen Zeitstempel wie createdAt oder updatedAt. Du kannst sie nicht mit einem festen Wert vergleichen, weil sie sich bei jedem Lauf ändern. Teste stattdessen ihre Eigenschaften:
- Die ID ist vorhanden, hat den richtigen Typ und unterscheidet sich von IDs, die du vorher angelegt hast.
- Ein
createdAtist ein gültiges Datum im dokumentierten Format und liegt wenige Sekunden um den Zeitpunkt deines Requests. Rechne mit Uhrabweichungen zwischen deinem Rechner und dem Server. - Ein
updatedAtändert sich nach einem Update,createdAtnicht. - Eine eigene
idim Body erlaubt dir nicht, sie zu wählen. Die Books-Dokumentation sagt, eine ID im Body wird ignoriert. Probier es mit"id": 1und prüf danach, dass Buch 1 unverändert ist.
Zurücklesen
Hol den Datensatz mit der ID, die du bekommen hast, und vergleich den ganzen Body mit dem, was du gesendet hast. Prüf dann die Liste. Bei dieser API kannst du nach dem Titel suchen:
/api/Books?q=web%20apisIst dein Buch nicht in den Ergebnissen, hat entweder die Antwort beim Anlegen gelogen, oder die Liste liest aus einer anderen Quelle als der Endpunkt für einzelne Datensätze (ein Cache, ein Suchindex, ein Lese-Replikat). Das alles kommt in echten Systemen vor. Die Liste zu prüfen, fängt auch ein leiseres Problem: Der Datensatz ist gespeichert, wird aber aus Listen herausgefiltert, etwa weil ein Standardfilter Datensätze mit einem leeren Feld ausblendet.
Mit PUT und PATCH aktualisieren
PUT ersetzt den ganzen Datensatz. Was du weglässt, wird zurückgesetzt, meist auf einen leeren oder Standardwert. Das wird ständig vergessen, auf Client- wie auf Serverseite, also teste es gezielt. Hier wird ein Buch mit nur drei Feldern aktualisiert:
curl -X PUT https://api.testingapis.com/api/Books/27 \
-H "Content-Type: application/json" \
-d '{"title": "Lessons Learned in Software Testing", "author": "Cem Kaner", "price": 39.0}'{
"message": "Book updated successfully",
"data": {
"id": 27,
"title": "Lessons Learned in Software Testing",
"author": "Cem Kaner",
"isbn": "",
"genre": "",
"publishedYear": 0,
"pages": 0,
"language": "",
"price": 39.0,
"rating": 0,
"available": false,
"tags": []
}
}ISBN, Seitenzahl, Jahr und Tags sind weg. Das ist korrektes PUT-Verhalten. Hätte der Server die alten Werte behalten, würde er sich wie PATCH verhalten, und ein Client, der sich darauf verlässt, mit PUT ein Feld zu leeren, würde das nie schaffen. Derselbe Request hat auch "id": 1 im Body gesendet; der Datensatz behielt die ID 27, und ein separates GET bestätigte, dass Buch 1 sich nicht verändert hatte.
Ein PUT, dem ein Pflichtfeld fehlt, scheitert mit 400, wie es sein soll: {"price": 35} allein liefert Missing required field: title.
PATCH ändert nur die Felder, die du schickst. Diese API nutzt JSON Merge Patch, und ihre Antwort listet, was sie geändert und was sie ignoriert hat:
curl -X PATCH https://api.testingapis.com/api/Books/26 \
-H "Content-Type: application/merge-patch+json" \
-d '{"price": 35, "available": false, "colour": "blue", "id": 999}'{
"message": "Book patched successfully",
"changed": ["price", "available"],
"ignored": ["colour", "id"],
"data": {
"id": 26,
"title": "Testing Web APIs",
"author": "Mark Winteringham",
"isbn": "9781617299537",
"genre": "testing",
"publishedYear": 2022,
"pages": 264,
"language": "English",
"price": 35,
"rating": 4.6,
"available": false,
"tags": ["api", "testing"]
}
}Prüf, dass sich die beiden genannten Felder geändert haben, dass jedes andere Feld seinen alten Wert behalten hat und dass die ignorierte id wirklich ignoriert wurde. Mach dann ein frisches GET und prüf alles noch einmal. Ein PATCH, das die Antwort aktualisiert, aber nicht den gespeicherten Datensatz, ist selten, doch wenn es passiert, ist es genau die Art Bug, die in Produktion landet, weil der Client den neuen Wert anzeigt, bis die Seite neu geladen wird.
Löschen, und noch einmal löschen
$ curl -i -X DELETE https://api.testingapis.com/api/Books/26
HTTP/1.1 200 OK
{"message":"Book deleted successfully"}
$ curl -i https://api.testingapis.com/api/Books/26
HTTP/1.1 404 Not Found
{"status":404,"error":"Not Found","message":"Book with ID 26 does not exist."}
$ curl -i -X DELETE https://api.testingapis.com/api/Books/26
HTTP/1.1 404 Not Found
{"status":404,"error":"Not Found","message":"Book with ID 26 does not exist."}Nach einem Löschen muss ein GET 404 liefern. Prüf auch, dass der Datensatz aus der Liste verschwunden ist und der Zähler gesunken ist (/api/Books/countanmelden zum Anzeigen liefert mit den Startdaten {"count": 25}).
Beim zweiten DELETE unterscheiden sich APIs. Diese liefert 404. Andere liefern bei jedem Löschen 204 oder 200, mit der Begründung, der Endzustand (kein solcher Datensatz) sei derselbe. Beides lässt sich vertreten. Du suchst nach einem 500 oder einem 200, das behauptet, etwas gelöscht zu haben, das gar nicht da war. Schick auch PUT und PATCH an die gelöschte ID. Hier bekommt PATCH ein 404, ein PUT mit unvollständigem Body aber ein 400 wegen des fehlenden Felds, bevor der Server merkt, dass der Datensatz nicht existiert. Welcher Fehler gewinnt, wenn zwei zutreffen, verdient einen Satz in der Dokumentation und hat oft keinen.
Leg eigene Daten an
Es ist verlockend, gegen Datensatz 1 zu testen, weil er immer da ist. Lass es. Auf einem geteilten Server kann jeder Buch 1 ändern oder löschen, und ein Test, der es bearbeitet, bricht jeden anderen Test, der es liest. Jeder Test sollte die Datensätze anlegen, die er braucht, nur mit den IDs arbeiten, die er bekommen hat, und sie am Ende löschen.
Diese API zeigt gut, warum. Sie hält ihre Daten im Speicher und wird von allen geteilt, die sie nutzen. Während diese Lektion entstand, hat ein anderer Nutzer in den wenigen Sekunden zwischen einem unserer Löschvorgänge und dem nächsten Anlegen ein Buch mit der ID 26 angelegt. Ein Test, der annahm, "das nächste Buch bekommt die 26", wäre aus Gründen gescheitert, die nichts mit dem getesteten Code zu tun hatten.
Dasselbe Experiment zeigte noch etwas. Nachdem Buch 26 gelöscht war, bekam das nächste angelegte Buch wieder die ID 26. Dieser Server vergibt die nächste freie Nummer, IDs werden also wiederverwendet. Hat ein anderes System vor dem Löschen "Buch 26" gespeichert, zeigt es jetzt auf ein anderes Buch. Ob das ein Bug ist, hängt vom Produkt ab, aber es ist die Art Fund, die man ansprechen sollte. Die meisten Datenbanken verwenden IDs aus genau diesem Grund nie wieder.
Bulk-Endpunkte
Viele APIs erlauben, mehrere Datensätze in einem Request anzulegen oder zu löschen. Spannend ist, was passiert, wenn manche Elemente fehlerhaft sind.
curl -X POST https://api.testingapis.com/api/Books/bulk \
-H "Content-Type: application/json" \
-d '[
{"title": "Bulk A", "author": "Tester"},
{"title": "Bulk B", "author": "Tester"},
{"title": "", "author": "Tester"}
]'
{"status":400,"error":"Bad Request","message":"Item [2]: Missing required field: title"}Das dritte Element hat einen leeren Titel, also scheitert der ganze Request, und der Fehler nennt das Element per Index. Die Books-Dokumentation sagt, dass in diesem Fall nichts gespeichert wird, und das solltest du überprüfen: Prüf, dass "Bulk A" nach dem gescheiterten Request nicht existiert. Eine API, die die ersten beiden Elemente speichert und dann scheitert, lässt den Client ratlos, was er wiederholen soll.
Bulk-Löschen meldet, welche IDs es gefunden hat und welche nicht:
curl -X DELETE "https://api.testingapis.com/api/Books/bulk?ids=27,28,5000"
{"message":"2 book record(s) deleted","deleted":[27,28],"notFound":[5000]}| Bulk-Fall | Was du prüfst |
|---|---|
| Alle Elemente gültig | Jedes Element wird gespeichert, die IDs sind verschieden, der Zähler steigt um die gesendete Anzahl. |
| Ein ungültiges Element | Nichts wird gespeichert, oder die Antwort sagt genau, welche Elemente gespeichert wurden. |
| Leeres Array | Ein klares 400, kein 201, das nichts angelegt hat. |
| Mehr als das Limit (hier 100) | Ein 400, das das Limit nennt. |
| Doppelte IDs beim Bulk-Löschen | Jeder Datensatz einmal gelöscht, kein Fehler beim zweiten Vorkommen. |
Übung
Spiel den ganzen Lebenszyklus mit /api/Moviesanmelden zum Anzeigen durch, mit seiner Referenzseite für die Felder. Leg einen Film mit allen Feldern an und dann:
- Lies ihn zurück und vergleich jedes Feld mit dem, was du gesendet hast.
- Finde ihn mit
?q=und einem Wort aus seinem Titel. - Schick ein PUT nur mit den Pflichtfeldern. Welche Felder wurden zurückgesetzt?
- Ändere mit PATCH ein Feld und schick einen Feldnamen mit, den es nicht gibt. Lies die Liste
ignored. - Lösch ihn, dann GET, PATCH und DELETE noch einmal. Notier jeden Status.
Schreib zum Schluss eine Sache auf, die du die Entwickler fragen würdest (ein ignoriertes Feld, eine wiederverwendete ID, ein Fehler, der sich gegen einen anderen durchgesetzt hat). Zu entscheiden, was du meldest, gehört genauso zum Test wie die Requests.