Node.js-Testtipps - fehlerhafte Anfragen bei REST-APIs mit TSOA
Im vierten Beitrag zu Node.js-Testtipps klopfen wir erneut unsere REST-Schnittstelle ab. Dieses Mal zeige ich, wie die Konfiguration des TSOA-Request-Bodys mithilfe fehlerhafter Testanfragen geprüft wird. Wie gewohnt gibt es Hands-on-Codebeispiele. Starten wir.
Für diese Tests gibt es ein paar Vorbedingungen, die wir erfüllen müssen. Zuerst benötigen wir das Setup, wie ich es in den Node.js Test-Tipps – REST-API mit CORS beschrieben habe. Damit wir fehlerhafte Eingaben prüfen können, ist unsere REST-API mit TSOA zu implementieren. Die relevanten Artikel dazu sind Node.js mit TSOA und Node.js mit TSOA und Request-Body-Validierung. Abschließend statten wir unsere Express-Applikation mit den notwendigen Handlern aus, um Node.js und RFC-konforme Fehlermeldungen zu erzeugen.
Ein kurzer Blick auf unseren Controller
Für die heutigen Tests definieren wir einen Controller mit einem Endpunkt zur Aktualisierung einer Entität. Die Typdefinition ist folgende:
export type UpdateBody = {
/**
* Anrede (Herr, Frau, Divers)
* @minLength 1 Bitte eine Anrede wählen
*/
salutation: Salutation // is an enum
/**
* Zusatzbezeichnung der Entität
* @minLength 1 Bitte einen Zusatz wählen
*/
degree: string
/**
* Vorname der Entität
* @minLength 1 Bitte einen Vornamen angeben
*/
firstName: string
/**
* Nachname der Entität
* @minLength 1 Bitte einen Nachnamen angeben
*/
lastName: string
/**
* Ein Spezialfall
* @pattern ^\d{6}$
*/
lanr: string
/**
* Die Anschrift als Freitext
* @minLength 1 Bitte eine Adresse angeben
*/
address: string
/**
* Die E-Mail-Adresse
*/
email: string | null
/**
* Die Faxnummer
*/
fax: string | null
}
Die dazugehörigen Tests
Ich habe die beste Erfahrung gemacht, wenn ich die Tests in zwei Blöcke strukturiere: Anfragen, die funktionieren sollen (valide sind) und Anfragen, bei denen ich von einem Fehler ausgehe (invalide sind). Weiterhin nutze ich das Sprachkonstrukt "it.each", um Codeduplikation zu vermeiden.
Ein allgemeiner Hinweis zu den Unit-Tests: Ich habe mir angewöhnt, alle meine Tests stets nach derselben Struktur zu schreiben: die drei As arrange, act und assert. Dadurch erleichtere ich mir das Lesen der Tests enorm. Genug beschrieben, kommen wir zum Testcode:
const baseUrl = '/entity'
describe('EntityController', () => {
describe('PUT update', () => {
// I use this URL for the whole test to reduce repetition
const apiUrl = baseUrl + '/entity-test-id'
// I define a working request body as base
const requestBody = {
salutation: 'HERR',
degree: 'Dr.',
firstName: 'Max',
lastName: 'Mustermann',
lanr: '123456',
address: 'Musterstraße 1, 12345 Musterstadt',
email: 'test@example.com',
fax: '123456789',
}
// This block contains all tests related to validation
describe('validation', () => {
describe('should work', () => {
it.each([
['email empty', { email: '' }], // Empty text for mail is allowed
['email empty', { email: null }], // Null as mail is allowed
['fax empty', { fax: '' }], // Empty text for fax is allowed
['fax empty', { fax: null }], // Null as fax is allowed
])('if %s', async (_, input) => {
// arrange
const testRequestBody = { ...requestBody, ... input }
// act
const { status, headers } = await agent.put(apiUrl).send(testRequestBody)
// assert
expect(status).toBe(200)
})
})
describe('should fail', () => {
it.each([
['salutation empty', { salutation: '' }, 'requestBody.salutation'],
['salutation unknown', { salutation: 'bogus' }, 'requestBody.salutation'],
['degree empty', { degree: '' }, 'requestBody.degree'],
['firstName empty', { firstName: '' }, 'requestBody.firstName'],
['lastName empty', { lastName: '' }, 'requestBody.lastName'],
['lanr empty', { lanr: '' }, 'requestBody.lanr'],
['lanr less than 6 digits', { lanr: '12345' }, 'requestBody.lanr'],
['lanr more than 6 digits', { lanr: '1234567' }, 'requestBody.lanr'],
['lanr not only digits', { lanr: '12345a' }, 'requestBody.lanr'],
['address empty', { address: '' }, 'requestBody.address'],
['email wrong format', { email: 'wrong-email.de' }, 'requestBody.email'],
])('if %s', async (_description, input, fieldName) => {
// arrange
const testRequestBody = { ...requestBody, ...input }
// act
const { status, body } = await agent.put(apiUrl).send(testRequestBody)
// assert
expect(status).toBe(422)
expect(body).toEqual({
detail: 'Validation failed',
status: 422,
invalidParameter: expect.anything(),
title: 'Invalid parameter',
type: 'validation-error',
})
expect(Object.keys(body.invalidParameter)).toContain(fieldName)
})
})
})
}
Hier noch ein paar Anmerkungen:
- Im Fehlerfall möchte ich sicherstellen, dass der Aufrufer den korrekten Fehlercode erhält. Ich habe mich dafür entschieden, stets den Code 422 Unprocessable Content zurückzugeben. Den Statuscode 400 nutze ich nur, wenn ich JSON erwarte, aber nicht erhalte.
- Des Weiteren prüfe ich die JSON-Struktur. Sie soll dem ProblemDocument entsprechen und die notwendigen Fehler bereitstellen. Hierbei ist mir besonders wichtig, dass in der Liste der invalidParameter-Einträge mein Fehler auftritt.
Fazit
Die Verknüpfung zwischen TSOA-Konfiguration und Testfällen ist deutlich geworden. Dank KI-Support beim Programmieren ist das Schreiben nicht so aufwendig, wie es im ersten Moment scheint. So offensichtlich die Tests auch sind, kann ich nicht empfehlen, sie wegzulassen. Zum einen dienen sie als Zusicherung dafür, dass die Vorgaben aus den Akzeptanzkriterien (so wie sie in den Stories definiert sind) eingehalten werden. Zum anderen ist bei späteren Änderungen an der Schnittstelle gewährleistet, dass Seiteneffekte frühzeitig erkannt werden.