Validierung und Fehler testen

Schlechte Eingaben, fehlende Felder, falsche Typen und wie ein guter Fehler aussieht.

Die letzte Lektion hat gute Daten geschickt und geprüft, dass sie gespeichert wurden. Diese schickt schlechte Daten und prüft, dass sie abgelehnt werden. In den meisten Projekten stecken hier die Bugs. Entwickler schreiben zuerst den Normalfall, testen ihn beim Bauen von Hand und haben dann keine Zeit mehr, bevor sie darüber nachgedacht haben, was ein Client stattdessen schicken könnte.

Ein Validierungstest hat zwei Teile. Der Server muss den Request mit dem richtigen Status ablehnen, und er darf nichts speichern. Danach zählt die Qualität der Fehlermeldung, denn irgendwann liest ein anderer Entwickler sie, während er herauszufinden versucht, was er falsch gemacht hat.

Ein Formular mit Regeln

POST /api/validation/registeranmelden zum Anzeigen ist ein Registrierungs-Endpunkt mit festgeschriebenen Regeln. Die Referenzseite listet sie auf: Der Benutzername hat 3 bis 20 Buchstaben, Ziffern oder Unterstriche, das Alter ist eine Ganzzahl von 18 bis 120, das Land ist einer von neun Codes und so weiter. Es wird nichts gespeichert, du kannst denselben Body also beliebig oft schicken. Ein gültiger Request:

POST/api/validation/register
{
  "username": "ava_t",
  "email": "ava@example.com",
  "password": "Str0ng!Pass",
  "confirmPassword": "Str0ng!Pass",
  "age": 29,
  "country": "us",
  "phone": "+15550101",
  "acceptTerms": true
}
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen
Antwort
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "message": "User registered successfully",
  "data": {
    "id": 1001,
    "username": "ava_t",
    "email": "ava@example.com",
    "age": 29,
    "country": "US",
    "createdAt": "2026-09-30T18:32:12Z"
  }
}

Beachte, dass "us" als "US" zurückkam. Der Server hat den Wert normalisiert, statt ihn abzulehnen. Das ist in Ordnung, aber es ist ein Verhalten, und ein Test sollte es festschreiben, damit es niemand versehentlich ändert.

Brich jetzt fast jede Regel auf einmal:

POST/api/validation/register
{
  "username": "ab",
  "email": "ava@example",
  "password": "password",
  "confirmPassword": "Password",
  "age": 17,
  "country": "XX",
  "acceptTerms": false
}
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen
Antwort
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://example.com/problems/validation-error",
  "title": "One or more fields failed validation.",
  "status": 422,
  "detail": "7 field(s) failed validation.",
  "instance": "/api/validation/register",
  "errors": {
    "username": ["Username must be 3-20 characters long."],
    "email": ["Email must be a valid email address."],
    "password": [
      "Password must contain an uppercase letter.",
      "Password must contain a digit.",
      "Password must contain a special character."
    ],
    "confirmPassword": ["Passwords do not match."],
    "age": ["Age must be between 18 and 120."],
    "country": ["Country must be one of: US, GB, IN, DE, FR, JP, CA, AU, BR."],
    "acceptTerms": ["You must accept the terms and conditions."]
  }
}
Dieses Beispiel nutzt Endpunkte für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen

Das ist eine gute Fehlerantwort, und es lohnt sich anzusehen, warum, damit du später eine schlechte erkennst.

  • Sie meldet jedes Problem auf einmal. Ein Client, der das Formular korrigiert, muss es nicht siebenmal abschicken, um sieben Fehler zu finden.
  • Jeder Fehler ist nach dem Feldnamen geschlüsselt, den der Client gesendet hat, sodass eine Oberfläche ihn neben dem richtigen Eingabefeld anzeigen kann.
  • Die Nachrichten nennen die Regel ("between 18 and 120"), statt nur zu sagen, dass der Wert falsch ist, und den Client raten zu lassen, warum.
  • Sie folgt RFC 7807 (aktualisiert als RFC 9457), einem Standardformat für HTTP-Fehler mit den Feldern type, title, status, detail und instance und dem Medientyp application/problem+json. Du musst nicht auf diesem Format bestehen, aber wenn eine API behauptet, es zu nutzen, prüf, dass die Felder da sind und dass status im Body zum echten Statuscode passt.

Was du schickst

Du wirst nicht bei jedem Feld alles ausprobieren. Geh jedes Feld durch und frag dich, welche davon zutreffen könnten:

Art der EingabeBeispiele
FehlendLass das Feld ganz weg. Schick es als null. Schick einen leeren String. Das kann sich unterschiedlich verhalten.
Falscher Typ"29" für eine Zahl, 29 für einen String, true für einen String, ein Objekt, wo eine Liste erwartet wird
GrenzenDas exakte Minimum und Maximum, je eins daneben, null, negative Zahlen, eine Dezimalzahl, wo eine Ganzzahl erwartet wird
LängeLeer, ein Zeichen, die Maximallänge, eins mehr und etwas sehr Langes (10.000 Zeichen)
FormatE-Mails, Daten, Telefonnummern, URLs und Codes, die fast stimmen
Leerzeichen und SchreibweiseFührende Leerzeichen, ein Titel nur aus Leerzeichen, Admin, wenn admin schon vergeben ist
Unbekannte FelderEin falsch geschriebener Feldname, ein zusätzliches Feld, ein Feld, das der Client nicht setzen können sollte
Der Body selbstKein Body, fehlerhaftes JSON, ein Array statt eines Objekts, der falsche Content-Type

Grenzen am Registrierungs-Endpunkt

Die Altersregel lautet 18 bis 120, die interessanten Werte sind also 17, 18, 120 und 121. Bei dieser API werden 18 und 120 angenommen, 17 und 121 bekommen "Age must be between 18 and 120." Ein Wert von 18.5bekommt eine andere Nachricht, "Age must be an integer.", und der String "29" ebenso. Manche APIs wandeln "29" stillschweigend in 29 um. Keines der beiden Verhalten ist an sich falsch, aber Clients werden sich auf das verlassen, das ausgeliefert wird.

Der Benutzername hat 3 bis 20 Zeichen: abc und ein Name mit 20 Buchstaben kommen durch, einer mit 21 scheitert. Ein Benutzername mit Leerzeichen wird mit einer Nachricht über erlaubte Zeichen abgelehnt. Der vergebene Name admin wird abgelehnt, Admin ebenfalls, was dir sagt, dass die Eindeutigkeitsprüfung Groß- und Kleinschreibung ignoriert. Täte sie das nicht, könnten zwei Nutzer Namen bekommen, die an den meisten Stellen gleich aussehen.

Die E-Mail-Prüfung ist das übliche Durcheinander. ava@example, ava@@example.com und ava.example.com werden abgelehnt, ebenso " ava@example.com" mit führendem Leerzeichen. ava+qa@example.com und AVA@EXAMPLE.COM werden angenommen, was richtig ist. a@b.c wird auch angenommen. Echte E-Mail-Validierung ist ein langer Streit, den niemand gewinnt, also geht es beim Test weniger um die perfekte Regel als um die Fälle, die deinem Produkt wichtig sind: Plus-Adressen, Großschreibung und ob umgebende Leerzeichen entfernt oder abgelehnt werden.

Leer ist nicht dasselbe wie fehlend

Schick {}, und jedes Pflichtfeld wird als "is required" gemeldet. Schick dieselben Felder mit null-Werten, und du bekommst dasselbe Ergebnis. Das ist konsistent, was gut ist. Bei /api/Booksanmelden zum Anzeigen wird ein Titel aus drei Leerzeichen ebenfalls als fehlend behandelt. Viele APIs prüfen nur, ob ein String existiert, sodass ein Titel aus Leerzeichen oder ein leerer String glatt durchgeht und später als leere Zeile in jemandes Tabelle auftaucht.

Längengrenzen fehlen oft ganz. /api/Booksanmelden zum Anzeigen hat einen Titel mit 5.000 Zeichen mit 201 angenommen. Es gibt keine dokumentierte Grenze, also mag das gewollt sein, aber eine Datenbankspalte oder ein UI-Layout wird später anderer Meinung sein. Frag nach, wo die Grenze liegen soll, und teste je ein Zeichen darüber und darunter.

Unbekannte Felder

Schickt ein Client ein Feld, das der Server nicht kennt, kann der Server es ignorieren oder ablehnen. In der CRUD-Lektion hat /api/Booksanmelden zum Anzeigen ein falsch geschriebenes Feld year ignoriert und das Jahr 0 gespeichert. /api/validation/strictanmelden zum Anzeigen lehnt stattdessen ab:

Terminal
curl -X POST https://api.testingapis.com/api/validation/strict \
  -H "Content-Type: application/json" \
  -d '{"name": "Widget", "email": "a@b.co", "quantity": 2, "discount": 50}'

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://example.com/problems/unknown-fields",
  "title": "Request body contains unknown fields.",
  "status": 400,
  "detail": "Allowed fields: name, email, quantity (names are case-sensitive).",
  "instance": "/api/validation/strict",
  "unknownFields": ["discount"]
}
Dieses Beispiel nutzt Endpunkte für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen

Feldnamen unterscheiden hier Groß- und Kleinschreibung, also ist Name statt name ebenfalls ein unbekanntes Feld. Ablehnen ist sicherer, weil Tippfehler sofort auffallen. Ignorieren ist freundlicher, wenn sich eine API weiterentwickelt und alte Clients Felder schicken, die entfernt wurden. Deine Aufgabe ist herauszufinden, welchen Ansatz die API nutzt und ob sie ihn über alle Endpunkte hinweg konsistent nutzt.

Eine Art unbekanntes Feld ist ein Sicherheitsthema statt einer Stilfrage: ein Feld, das es am Datensatz gibt, das der Client aber nicht setzen können soll, etwa role, isAdmin, price bei einer Bestellung oder id. Kopiert der Server jedes eingehende Feld auf den Datensatz, kann ein Nutzer sich selbst befördern. Die Sicherheitslektion nennt das Mass Assignment.

Kaputte Bodies

Teste als Nächstes den Request als Ganzes. Das sind echte Antworten des Registrierungs-Endpunkts:

Antworten
# No body at all
HTTP/1.1 400 Bad Request
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"":["A non-empty request body is required."]},"traceId":"00-fc01be6d..."}

# {"username":"ava_t",}  (trailing comma)
HTTP/1.1 400 Bad Request
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"$":["The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 0 | BytePositionInLine: 20."]},"traceId":"00-e13ac011..."}

# [] instead of an object
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"status":400,"error":"Bad Request","message":"Request body must be a JSON object."}

# Valid JSON sent as Content-Type: text/plain
HTTP/1.1 415 Unsupported Media Type
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.16","title":"Unsupported Media Type","status":415,"traceId":"00-4cadc1e1..."}

Alle vier werden behandelt, und 415 für den falschen Content-Type ist genau richtig. Aber sieh dir die Formen an. Drei Antworten sind problem+json mit einer errors-Map. Der Array-Fall liefert ein ganz anderes Format, {status, error, message}, mit einem einfachen JSON-Content-Type. Ein Client, der Fehler auf die eine Weise parst, bricht bei der anderen. Das passiert, wenn manche Fehler vom Framework kommen und andere vom Anwendungscode, und es ist einer der häufigsten Befunde beim API-Testing. Sammle die Fehlerformen, die dir in einer API begegnen, und leg sie nebeneinander.

Was ein Fehler nicht enthalten sollte

So reagiert /api/Booksanmelden zum Anzeigen, wenn es einen String bekommt, wo es eine Zahl erwartet:

Terminal
curl -X POST https://api.testingapis.com/api/Books \
  -H "Content-Type: application/json" \
  -d '{"title": "x", "author": "y", "pages": "many"}'

HTTP/1.1 400 Bad Request

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "item": ["The item field is required."],
    "$.pages": ["The JSON value could not be converted to System.Int32. Path: $.pages | LineNumber: 0 | BytePositionInLine: 40."]
  },
  "traceId": "00-d027cc55a2925746a1bf93cc96d2306d-02234fa55d940604-00"
}
Dieses Beispiel nutzt Endpunkte für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen

Der Status stimmt. Der Inhalt hat zwei Probleme. Die Nachricht erwähnt System.Int32, was Außenstehenden verrät, dass der Server auf .NET läuft, und dem Client einen Typnamen statt "pages muss eine ganze Zahl sein" hinterlässt. Und der zusätzliche Fehler "The item field is required." bezieht sich auf einen internen Parameternamen, nicht auf etwas, das der Client gesendet hat. Beides ist hier nicht gefährlich, aber bei einem echten Produkt würdest du beides melden.

Was nie in einem Fehler auftauchen darf: Stacktraces, SQL-Fragmente, Dateipfade, interne Hostnamen und die Werte von Geheimnissen. Eine traceId dagegen ist nützlich: Mit ihr findet der Support den passenden Server-Log, ohne dass etwas preisgegeben wird.

Welcher Statuscode

Teams streiten über 400 gegenüber 422. Eine gängige Aufteilung ist 400 für einen Body, den der Server nicht lesen konnte (fehlerhaftes JSON, falsche Typen), und 422 für einen lesbaren Body, der eine Regel verletzt (Alter 17). /api/validation/registeranmelden zum Anzeigen liefert 400 nur, wenn es den Body gar nicht parsen kann, und 422 für alles andere, einschließlich eines Strings, wo das Alter stehen sollte. /api/validation/annotationsanmelden zum Anzeigen nutzt für alles 400. Beide Ansätze sind akzeptabel, wenn sie konsistent und dokumentiert sind, und zwei Endpunkte derselben API, die es unterschiedlich machen, sind eine Frage wert.

StatusBedeutung bei schlechter Eingabe
400 Bad RequestDer Request ist fehlerhaft aufgebaut oder ungültig. Der Client muss ihn ändern.
415 Unsupported Media TypeDer Content-Type gehört nicht zu denen, die der Endpunkt annimmt.
422 Unprocessable ContentDer Body ließ sich parsen, aber die Werte verletzen eine Regel.
409 ConflictFür sich gültig, kollidiert aber mit dem aktuellen Zustand, etwa ein Duplikat. (Der Registrierungs-Endpunkt nutzt 422 für einen vergebenen Benutzernamen.)
500 Internal Server ErrorBei schlechter Eingabe nie richtig. Der Server ist an etwas abgestürzt, das er hätte ablehnen sollen.

Die letzte Zeile ist die eine Regel ohne Ausnahmen. Wenn irgendeine Eingabe ein 500 auslöst, hast du einen Bug gefunden, egal wie merkwürdig die Eingabe war. Der Server sollte sie ablehnen und ist stattdessen umgefallen. Wie ein sauber aufgebautes 500 aussieht, siehst du unter /api/validation/problem/internalanmelden zum Anzeigen, und die anderen Beispiel-Problemdokumente sind hier aufgelistet:

GET/api/validation/problem
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen

Übung

Teste POST /api/validation/annotationsanmelden zum Anzeigen. Sein gültiges Beispiel steht auf der Referenzseite. Es nutzt die eingebaute Validierung des Frameworks statt handgeschriebener Regeln.

  • Finde die Grenzen für username, age und tags.
  • Probier drei ungültige und zwei gültige Telefonnummern und Websites.
  • Schick einen ungültigen Body an /annotations und an /register und vergleich die Fehlerantworten. Welchen Status nutzt jeder? Wie sind die Feldnamen in errors im Vergleich zu den gesendeten geschrieben?
  • Schick einen String, wo age eine Zahl erwartet. Verrät die Nachricht etwas?

Schreib die Unterschiede zwischen den beiden Endpunkten so auf, als würdest du sie für einen Entwickler melden: was du gesendet hast, was zurückkam und was du erwartet hast.

Üben und weitergehen