Unit Tests for Node.js APIs built with TS, Express.js and TypeORM

Unit Tests for Node.js APIs built with TS, Express.js and TypeORM

Some days ago I wrote an article about how I structure my Node.js REST APIs. However, I didn’t cover any test scenarios in there. So it’s time to catch up on this now.

We’re going to write an unit test for a single API component based on the project structure from my other article. The goal is to test the component by mocking a database and sending an HTTP request to its routes.

For writing tests I use the following node modules:

  • Mocha
  • Chai
  • Supertest

Project structure

This is the project structure I mentioned above. Of course, you can use any other as well.

nodejs-api-structure
└───src
   │
   └───config
   │
   └───api
   │   │
   │   └───components
   │   │   │
   │   │   └───user
   │   │       │   controller.ts
   │   │       │   model.ts
   │   │       │   routes.ts
   │   │       │   service.ts
   │   │       │   user.spec.ts
   │   │
   │   └───middleware
   │   │
   │   │   routes.ts
   │   │   server.ts
   │
   └───test
   │   │   factory.ts
   │
   │   index.ts

We'll focus on the following files:

  • factory.ts
  • user.spec.ts

Test Factory (factory.ts)

This file is some kind of a setup file for each single unit test. It takes care of the database connection and starts the Express.js server.

We use ‘sqljs’ as database type, so it’s not necessary to provide a real database like MySQL or any other.

The code should be self explanatory. The class acts like a container for the database connection and express server. It provides getter methods to make them accessible and a method to open / close the connections.

import 'reflect-metadata';
import 'source-map-support/register';
import 'module-alias/register';

// Set env to test
process.env.NODE_ENV = 'test';

// Set env variables from .env file
import { config } from 'dotenv';
config();

import { createConnection, ConnectionOptions, Connection } from 'typeorm';
import { createServer, Server as HttpServer } from 'http';

import express from 'express';
import supertest from 'supertest';

import { env } from '@config/globals';

import { Server } from '../api/server';

/**
 * TestFactory
 * - Loaded in each unit test
 * - Starts server and DB connection
 */

export class TestFactory {
    private _app: express.Application;
    private _connection: Connection;
    private _server: HttpServer;

    // DB connection options
    private options: ConnectionOptions = {
        type: 'sqljs',
        database: new Uint8Array(),
        location: 'database',
        logging: false,
        synchronize: true,
        entities: ['dist/api/components/**/model.js']
    };

    public get app(): supertest.SuperTest<supertest.Test> {
        return supertest(this._app);
    }

    public get connection(): Connection {
        return this._connection;
    }

    public get server(): HttpServer {
        return this._server;
    }

    /**
     * Connect to DB and start server
     */
    public async init(): Promise<void> {
        this._connection = await createConnection(this.options);
        this._app = new Server().app;
        this._server = createServer(this._app).listen(env.NODE_PORT);
    }

    /**
     * Close server and DB connection
     */
    public async close(): Promise<void> {
        this._server.close();
        this._connection.close();
    }
}

Component test (user.spec.ts)

This file covers the unit test for the API component. In there, we use different HTTP request methods like POST, PUT, GET and DELETE to test the component’s API endpoints.

First of all, we create a new instance of the TestFactory class and User model. The mockTestUser methods returns an instance of User including some dummy data. Moreover we create another instance testUserModified with some modified properties which will be used to test the PUT endpoints.

const factory: TestFactory = new TestFactory();
const testUser: User = User.mockTestUser();
const testUserModified: User = { ...testUser, firstname: 'testFirstnameModified', lastname: 'testLastnameModified' };

Now we define Mocha’s before and after methods. before is executed before the test starts and after is executed after the test has ended.

Inside them we call the factory’s init and close method which establish a new database connection and express server before the test starts and disconnects from it when he has ended.

before(async () => {
    await factory.init();
});

after(async () => {
    await factory.close();
});

One important thing to notice is when you have multiple unit tests, that each test establishes a new database connection and express server.

For making HTTP requests to the server I use Supertest and Chai for validating the server responses.

Here's the complete code for one component:

import 'module-alias/register';

import { assert } from 'chai';

import { User } from './model';
import { TestFactory } from '../../../../test/factory';

describe('Testing user component', () => {
    // Create instances
    const factory: TestFactory = new TestFactory();
    const testUser: User = User.mockTestUser();
    const testUserModified: User = { ...testUser, firstname: 'testFirstnameModified', lastname: 'testLastnameModified' };

    before(async () => {
        await factory.init();
    });

    after(async () => {
        await factory.close();
    });

    describe('POST /users', () => {
        it('responds with status 400', (done) => {
            factory.app
                .post('/api/v1/users')
                .send()
                .set('Accept', 'application/json')
                .expect('Content-Type', /json/)
                .expect(400, done);
        });

        it('responds with new user', (done) => {
            factory.app
                .post('/api/v1/users')
                .send({
                    user: testUser
                })
                .set('Accept', 'application/json')
                .expect('Content-Type', /json/)
                .expect(200)
                .end((err, res) => {
                    try {
                        if (err) throw err;

                        const { status } = res.body;
                        const user: User = res.body.data;

                        // Assert status
                        assert(status === res.status, 'status does not match');

                        // Assert user
                        assert.isObject(user, 'user should be an object');
                        assert(user.id === testUser.id, 'userID does not match');
                        assert(user.email === testUser.email, 'userEmail does not match');
                        assert(user.firstname === testUser.firstname, 'userFirstname does not match');
                        assert(user.lastname === testUser.lastname, 'userLastname does not match');

                        return done();
                    } catch (err) {
                        return done(err);
                    }
                });
        });
    });

    describe('PUT /users/1', () => {
        it('responds with updated user', (done) => {
            factory.app
                .put('/api/v1/users/1')
                .send({
                    user: testUserModified
                })
                .set('Accept', 'application/json')
                .expect('Content-Type', /json/)
                .end((err, res) => {
                    try {
                        if (err) throw err;

                        const { status } = res.body;
                        const user: User = res.body.data;

                        // Assert status
                        assert(status === res.status, 'status does not match');

                        // Assert user
                        assert.isObject(user, 'user should be an object');
                        assert(user.id === testUserModified.id, 'userID does not match');
                        assert(user.email === testUserModified.email, 'userEmail does not match');
                        assert(user.firstname === testUserModified.firstname, 'userFirstname does not match');
                        assert(user.lastname === testUserModified.lastname, 'userLastname does not match');

                        return done();
                    } catch (err) {
                        return done(err);
                    }
                });
        });
    });

    describe('GET /users', () => {
        it('responds with user array', (done) => {
            factory.app
                .get('/api/v1/users')
                .set('Accept', 'application/json')
                .expect('Content-Type', /json/)
                .expect(200)
                .end((err, res) => {
                    try {
                        if (err) throw err;

                        const { status } = res.body;
                        const users: User[] = res.body.data;

                        // Assert status
                        assert(status === res.status, 'status does not match');

                        // Assert users
                        assert.isArray(users, 'users should be an array');
                        assert(users[0].id === testUserModified.id, 'userID does not match');
                        assert(users[0].email === testUserModified.email, 'userEmail does not match');
                        assert(users[0].firstname === testUserModified.firstname, 'userFirstname does not match');
                        assert(users[0].lastname === testUserModified.lastname, 'userLastname does not match');

                        return done();
                    } catch (err) {
                        return done(err);
                    }
                });
        });
    });

    describe('GET /users/1', () => {
        it('responds with single user', (done) => {
            factory.app
                .get('/api/v1/users/1')
                .set('Accept', 'application/json')
                .expect('Content-Type', /json/)
                .expect(200)
                .end((err, res) => {
                    try {
                        if (err) throw err;

                        const { status } = res.body;
                        const user: User = res.body.data;

                        // Assert status
                        assert(status === res.status, 'status does not match');

                        // Assert user
                        assert.isObject(user, 'user should be an object');
                        assert(user.id === testUserModified.id, 'userID does not match');
                        assert(user.email === testUserModified.email, 'userEmail does not match');
                        assert(user.firstname === testUserModified.firstname, 'userFirstname does not match');
                        assert(user.lastname === testUserModified.lastname, 'userLastname does not match');

                        return done();
                    } catch (err) {
                        return done(err);
                    }
                });
        });
    });

    describe('DELETE /users/1', () => {
        it('responds with status 204', (done) => {
            factory.app
                .delete('/api/v1/users/1')
                .set('Accept', 'application/json')
                .expect(204, done);
        });

        it('responds with status 404', (done) => {
            factory.app
                .delete('/api/v1/users/1')
                .set('Accept', 'application/json')
                .expect(404, done);
        });
    });
});

That's it! I hope this is a little help for people who struggle with writing unit tests for REST APIs built with Express.js and TypeORM.

I'm currently working on a side project where some components cover unit test as I described here. Have a look! at the 'user' or 'task' component for example.