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.

4 Minuten

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.

call to action background image

Abonniere meinen Newsletter

Erhalte einmal im Monat Nachrichten aus den Bereichen Softwareentwicklung und Kommunikation gespikt mit Buch- und Linkempfehlungen.