Lars Wächter

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

January 08, 2020

Some time 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"

// 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 testUserUpdated: 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 "reflect-metadata"
import { assert, expect } from "chai"

import { TestFactory } from "../../../../test/factory"
import { User } from "./model"

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(done => {
    factory.init().then(done)
  })

  after(done => {
    factory.close().then(done)
  })

  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({
          email: testUser.email,
          firstname: testUser.firstname,
          lastname: testUser.lastname,
          password: testUser.password,
          active: testUser.active,
        })
        .set("Accept", "application/json")
        .expect("Content-Type", /json/)
        .expect(200)
        .end((err, res) => {
          try {
            if (err) throw err

            const user: User = res.body

            assert.isObject(user, "user should be an object")

            expect(user.id).eq(testUser.id, "id does not match")
            expect(user.email).eq(testUser.email, "email does not match")
            expect(user.firstname).eq(
              testUser.firstname,
              "firstname does not match"
            )
            expect(user.lastname).eq(
              testUser.lastname,
              "lastname does not match"
            )
            expect(user.active).eq(testUser.active, "active 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({
          email: testUserUpdated.email,
          firstname: testUserUpdated.firstname,
          lastname: testUserUpdated.lastname,
          password: testUserUpdated.password,
          active: testUserUpdated.active,
        })
        .set("Accept", "application/json")
        .expect("Content-Type", /json/)
        .end((err, res) => {
          try {
            if (err) throw err

            const user: User = res.body

            assert.isObject(user, "user should be an object")

            expect(user.id).eq(testUserUpdated.id, "id does not match")
            expect(user.email).eq(testUserUpdated.email, "email does not match")
            expect(user.firstname).eq(
              testUserUpdated.firstname,
              "firstname does not match"
            )
            expect(user.lastname).eq(
              testUserUpdated.lastname,
              "lastname does not match"
            )
            expect(user.active).eq(
              testUserUpdated.active,
              "active 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 users: User[] = res.body

            assert.isArray(users, "users should be an array")

            expect(users[0].id).eq(testUserUpdated.id, "id does not match")
            expect(users[0].email).eq(
              testUserUpdated.email,
              "email does not match"
            )
            expect(users[0].firstname).eq(
              testUserUpdated.firstname,
              "firstname does not match"
            )
            expect(users[0].lastname).eq(
              testUserUpdated.lastname,
              "lastname does not match"
            )
            expect(users[0].active).eq(
              testUserUpdated.active,
              "active 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 user: User = res.body

            assert.isObject(user, "user should be an object")

            expect(user.id).eq(testUserUpdated.id, "id does not match")
            expect(user.email).eq(testUserUpdated.email, "email does not match")
            expect(user.firstname).eq(
              testUserUpdated.firstname,
              "firstname does not match"
            )
            expect(user.lastname).eq(
              testUserUpdated.lastname,
              "lastname does not match"
            )
            expect(user.active).eq(
              testUserUpdated.active,
              "active does not match"
            )

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

  describe("GET /users/search", () => {
    it("responds with single user", done => {
      factory.app
        .get("/api/v1/users/search")
        .query({ email: testUserUpdated.email })
        .set("Accept", "application/json")
        .expect("Content-Type", /json/)
        .expect(200)
        .end((err, res) => {
          try {
            if (err) throw err

            const user: User = res.body

            assert.isObject(user, "user should be an object")

            expect(user.id).eq(testUserUpdated.id, "id does not match")
            expect(user.email).eq(testUserUpdated.email, "email does not match")
            expect(user.firstname).eq(
              testUserUpdated.firstname,
              "firstname does not match"
            )
            expect(user.lastname).eq(
              testUserUpdated.lastname,
              "lastname does not match"
            )
            expect(user.active).eq(
              testUserUpdated.active,
              "active 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!

There’s also a GitHub repository available including an example application with this kind of tests for multiple components. Have a look.


This is my personal blog where I mostly write about technical or computer science based topics. Check out my GitHub profile too.