Node.js Test Tips - Invalid requests to REST-APIs build with TSOA

In the fourth part of my Node.js testing tips, we will once again examine our REST interface. This time, I'll demonstrate how to check the configuration of the TSOA request body using faulty test requests. As usual, there are hands-on code examples. Let's get started.

4 minutes

There are a few prerequisites we need to fulfil for these tests. First, we need the setup as I described in the Node.js Test Tips – REST API with CORS. To be able to check for invalid input, our REST API needs to be implemented with TSOA. The relevant articles for this are Node.js with TSOA and Node.js with TSOA and Request Body Validation. Finally, we equip our Express application with the necessary handlers to generate Node.js and RFC-compliant error messages.

A brief look at our controller

For today's tests, we define a controller with an endpoint for updating an entity. The type definition is as follows:

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  
}

The accompanying tests

I've found it works best to structure the tests into two blocks: requests that should work (valid) and requests that I expect to fail (invalid). I also use the it.each language construct to avoid code duplication.

A general note about unit tests: I've made it a habit to always write all my tests using the same structure: the three Aarrange, act, and assert. This makes the tests much easier to read. Enough said, let's look at the test code:

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)
      })
    })
  })
}

Here are a few more notes:

  • In case of an error, I want to ensure that the caller receives the correct error code. I've decided to always return the code 422 Unprocessable Content. I only use the status code 400 if I expect JSON but don't receive it.
  • Furthermore, I check the JSON structure. It should correspond to the ProblemDocument and provide the necessary errors. It's particularly important to me that my error appears in the list of invalidParameter entries.

Conclusion

The connection between TSOA configuration and test cases has become clear. Thanks to AI support in programming, writing them isn't as time-consuming as it initially seems. As obvious as the tests are, I can't recommend omitting them. Firstly, they serve as assurance that the requirements from the acceptance criteria (as defined in the user stories) are met. Secondly, they ensure that any side effects are detected early on if the interface is modified later.

call to action background image

Subscribe to my newsletter

Receive once a month news from the areas of software development and communication peppered with book and link recommendations.