author

Kien Duong

April 30, 2022

Testing NodeJS project with Jest

1. Setup Jest

Documentation: https://jestjs.io/docs/getting-started

Install Jest package: yarn add jest

Inside the root project, create jest.config.json file. Create one more file that contains jest config at src/configs/testing.config.ts

In the package.json file, create a new command to run testing:

    // jest.config.json
{
  "verbose": true,
  "rootDir": ".",
  "preset": "ts-jest",
  "testMatch": ["**/*.spec.ts", "**/*.test.ts"],
  "coveragePathIgnorePatterns": ["mocks/"],
  "moduleFileExtensions": ["ts", "js"],
  "testEnvironment": "node",
  "clearMocks": true,
  "setupFiles": ["./src/configs/testing.config.ts"]
}

  
    // testing.config.ts
require('dotenv').config();
  
    // package.json
...
"scripts": {
   ...
   "test": "jest --collectCoverage",
   ...
},
...
  

2. Unit Testing Services

In order to test entities, we must setup a small sqlite database that will be setup before the test and destroyed after the test.

Install sqlite package: yarn add -D better-sqlite3 @types/better-sqlite3

Inside src/configs folder, we create a new file sqlite.config.ts to handle testing DB.

Inside src/services/user folder, we create a new testing file user.service.test.ts that contains unit test logic for user service.

For using sqlite database in unit test file, we must setup db instance inside beforeAll & destroy db instance inside afterAll.

    // sqlite.config.ts
import { Connection, createConnection } from 'typeorm';
import Database from 'better-sqlite3';

export class SQLite {

  private static db: SQLite;

  public static get instance(): SQLite {
    if (!this.db) {
      this.db = new SQLite();
    }
    return this.db;
  }

  private dbConnection!: Connection;
  private sqliteDB!: any;

  async setup() {
    this.sqliteDB = new Database(':memory:', { verbose: console.log });
    this.dbConnection = await createConnection({
      name: 'default',
      type: 'better-sqlite3',
      database: ':memory:',
      entities: ['src/entities/**/*.ts'],
      synchronize: true,
    });
  }

  destroy() {
    this.dbConnection.close();
    this.sqliteDB.close();
  }

}

  
    // user.service.test.ts
// Configs
import SQLite from '../../configs/sqlite.config';

// Services
import userService from './user.service';

let user: any;

beforeAll(async () => {
  user = {
    email: 'admin@gmail.com',
    password: 'password',
    firstName: 'First',
    lastName: 'Last',
  }
  await SQLite.instance.setup();
});

afterAll(() => {
  SQLite.instance.destroy();
});

describe('Testing user service', () => {
  test('Create a new user', async () => {
    const result = await userService.create(user);
    expect(result.id).toBe(1);
    expect(result.email).toBe(user.email);
    expect(result).toHaveProperty('password', undefined);
    expect(result.firstName).toBe(user.firstName);
    expect(result.lastName).toBe(user.lastName);
    expect(result).toHaveProperty('createdAt');
    expect(result).toHaveProperty('updatedAt');
  });

  test('User login', async () => {
    const result = await userService.login({
      email: user.email,
      password: user.password,
    });
    expect(result.id).toBe(1);
    expect(result.email).toBe(user.email);
    expect(result).toHaveProperty('password', undefined);
    expect(result.firstName).toBe(user.firstName);
    expect(result.lastName).toBe(user.lastName);
    expect(result).toHaveProperty('createdAt');
    expect(result).toHaveProperty('updatedAt');
  });

  test('Login with incorrect user', async () => {
    try {
      await userService.login({
        email: 'fake@gmail.com',
        password: user.password,
      });
    } catch (e) {
      expect(e.message).toBe('Your email has not been registered');
    }
  });

  test('Login with incorrect password', async () => {
    try {
      await userService.login({
        email: user.email,
        password: 'fake',
      });
    } catch (e) {
      expect(e.message).toBe('Your password is not correct');
    }
  });

  test('Get user by id', async () => {
    const result = await userService.getById({ id: 1 });
    expect(result.id).toBe(1);
    expect(result.email).toBe(user.email);
    expect(result).toHaveProperty('password', undefined);
    expect(result.firstName).toBe(user.firstName);
    expect(result.lastName).toBe(user.lastName);
    expect(result).toHaveProperty('createdAt');
    expect(result).toHaveProperty('updatedAt');
  });

  test('Get user by incorrect id', async () => {
    const result = await userService.getById({ id: 100 });
    expect(result).toBe(null);
  });

  test('Get user detail', async () => {
    const result = await userService.detail({ id: 1 });
    expect(result.id).toBe(1);
    expect(result.email).toBe(user.email);
    expect(result).toHaveProperty('password', undefined);
    expect(result.firstName).toBe(user.firstName);
    expect(result.lastName).toBe(user.lastName);
    expect(result).toHaveProperty('createdAt');
    expect(result).toHaveProperty('updatedAt');
  });

  test('Get user detail by incorrect id', async () => {
    try {
      await userService.detail({ id: 100 });
    } catch (e) {
      expect(e.message).toBe('User is not existed');
    }
  });

  test('Update user', async () => {
    await userService.update({
      id: 1,
      firstName: 'First2',
      lastName: 'Last2',
    });
    const result = await userService.getById({ id: 1 });
    expect(result.firstName).toBe('First2');
    expect(result.lastName).toBe('Last2');
  });

  test('Update incorrect user', async () => {
    try {
      await userService.update({
        id: 100,
        firstName: 'First2',
        lastName: 'Last2',
      });
    } catch (e) {
      expect(e.message).toBe('User is not existed');
    }
  });

  test('Get list users', async () => {
    const result = await userService.list({
      page: 1,
      limit: 5,
    });
    expect(result).toHaveProperty('response');
    expect(result).toHaveProperty('pagination');
    expect(Array.isArray(result.response)).toBe(true);
    expect(result.response.length).toBe(1);
  });

  test('Get list users with keyword', async () => {
    const result = await userService.list({
      page: 1,
      limit: 5,
      keyword: 'F',
    });
    expect(result).toHaveProperty('response');
    expect(result).toHaveProperty('pagination');
    expect(Array.isArray(result.response)).toBe(true);
    expect(result.response.length).toBe(1);
  });

  test('Remove a user', async () => {
    const result = await userService.remove({ id: 1 });
    expect(result).toBeDefined();
  });

  test('Remove incorrect user', async () => {
    try {
      await userService.remove({ id: 100 });
    } catch (e) {
      expect(e.message).toBe('User is not existed');
    }
  });
});

  

Running yarn test to see the result. As you can see, all test cases for user service have been passed.

nodejs jest 1

3. Unit Testing Controllers

Inside src/controllers/user folder, we create a new testing file user.controller.test.ts that contains unit test logic for user controller.

In order to test the controllers, we must create the mock request & response that will be passed to the controllers. Inside src/mocks folder, create a new file api.mock.ts

nodejs jest 2

    // api.mock.ts
const mockRequest = () => {
  const req: any = {};
  req.body = jest.fn().mockReturnValue(req);
  req.params = jest.fn().mockReturnValue(req);
  return req;
}

const mockResponse = () => {
  const res: any = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  res.cookie = jest.fn().mockReturnValue(res);
  return res;
};

export default {
  mockRequest,
  mockResponse,
}

  
    // user.controller.test.ts
// Configs
import SQLite from '../../configs/sqlite.config';
import mockApi from '../../mocks/api.mock';

// Controllers
import userController from './user.controller';

let req: any;
let res: any;

beforeAll(async () => {
  await SQLite.instance.setup();
});

afterAll(() => {
  SQLite.instance.destroy();
});

describe('Testing user controller', () => {
  beforeEach(() => {
    req = mockApi.mockRequest();
    res = mockApi.mockResponse();
  });

  afterEach(() => {
    req = null;
    res = null;
  });

  test('Create user', async () => {
    req.body = {
      email: 'admin@gmail.com',
      password: 'password',
      firstName: 'First',
      lastName: 'Last',
    }
    await userController.create(req, res);
    expect(res.status).toHaveBeenCalledWith(201);
  });

  test('Create duplicated user', async () => {
    req.body = {
      email: 'admin@gmail.com',
      password: 'password',
      firstName: 'First',
      lastName: 'Last',
    }
    await userController.create(req, res);
    expect(res.status).toHaveBeenCalledWith(409);
  });

  test('Create incorrect user', async () => {
    await userController.create(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  test('User login', async () => {
    req.body = {
      email: 'admin@gmail.com',
      password: 'password',
    }
    await userController.login(req, res);
    expect(res.status).toHaveBeenCalledWith(200);
  });

  test('User login with wrong data', async () => {
    req.body = {
      email: 'fake@gmail.com',
      password: 'password',
    }
    await userController.login(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  test('User login with empty data', async () => {
    req.body = null;
    await userController.login(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  test('Get current user', async () => {
    req.user = {
      id: 1,
    }
    await userController.me(req, res);
    expect(res.status).toHaveBeenCalledWith(200);
  });

  test('Get user detail', async () => {
    req.params = {
      id: 1,
    }
    await userController.detail(req, res);
    expect(res.status).toHaveBeenCalledWith(200);
  });

  test('Get user detail with wrong id', async () => {
    req.params = {
      id: 100,
    }
    await userController.detail(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  test('Update user', async () => {
    req.body = {
      firstName: 'First',
      lastName: 'Last',
    }
    req.params = {
      id: 1,
    }
    await userController.update(req, res);
    expect(res.status).toHaveBeenCalledWith(200);
  });

  test('Update user with wrong data', async () => {
    req.body = null;
    await userController.update(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  test('Update current user', async () => {
    req.body = {
      firstName: 'First',
      lastName: 'Last',
    }
    req.user = {
      id: 1,
    }
    await userController.updateMe(req, res);
    expect(res.status).toHaveBeenCalledWith(200);
  });

  test('Update current user with wrong data', async () => {
    req.body = null;
    await userController.updateMe(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  test('Get list users', async () => {
    req.query = {
      limit: 5,
      page: 1,
    }
    await userController.list(req, res);
    expect(res.status).toHaveBeenCalledWith(200);
  });

  test('Get list users with wrong query params', async () => {
    req.query = null;
    await userController.list(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  test('Remove a user', async () => {
    req.params = {
      id: 1,
    }
    await userController.remove(req, res);
    expect(res.status).toHaveBeenCalledWith(200);
  });

  test('Remove a wrong', async () => {
    req.params = null;
    await userController.remove(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
  });
});

  

4. Unit Testing Routes

Install HTTP testing packages: yarn add -D supertest @types/supertest

For testing the endpoints that must have the authorization, we must create a default user & generate the cookie inside beforeAll. Create 4 new testing files for default, auth, me & user routers.

  • src/routes/default/default.route.test.ts
  • src/routes/auth/auth.route.test.ts
  • src/routes/me/me.route.test.ts
  • src/routes/user/user.route.test.ts

nodejs jest 3

    // default.route.test.ts
import request, { CallbackHandler } from 'supertest';
import routes from './default.route';
import app from '../../configs/express.config';

describe('Testing default router', () => {
  test('Get default', (done: CallbackHandler) => {
    return request(app.use(routes))
      .get('/')
      .expect(200)
      .expect((res: any) => {
        const result = JSON.parse(res.text);
        expect(result).toHaveProperty('message');
      })
      .end(done);
  });
});

  
    // auth.route.test.ts
import request, { CallbackHandler } from 'supertest';
import routes from './auth.route';
import app from '../../configs/express.config';
import SQLite from '../../configs/sqlite.config';

beforeAll(async () => {
  await SQLite.instance.setup();
});

afterAll(() => {
  SQLite.instance.destroy();
});

describe('Testing auth router', () => {
  test('Register a new user', (done: CallbackHandler) => {
    const user = {
      email: 'admin@gmail.com',
      password: 'password',
      firstName: 'First',
      lastName: 'Last',
    };
    return request(app.use(routes))
      .post('/auth/register')
      .send(user)
      .expect(201)
      .expect((res: any) => {
        const result = JSON.parse(res.text);
        expect(result).toHaveProperty('data');
        expect(result).toHaveProperty('success', true);
        expect(result.data.id).toBe(1);
        expect(result.data.email).toBe(user.email);
        expect(result.data.firstName).toBe(user.firstName);
        expect(result.data.lastName).toBe(user.lastName);
        expect(result.data).toHaveProperty('createdAt');
        expect(result.data).toHaveProperty('updatedAt');
      })
      .end(done);
  });

  test('User login', (done: CallbackHandler) => {
    const user = {
      email: 'admin@gmail.com',
      password: 'password',
    };
    return request(app.use(routes))
      .post('/auth/login')
      .send(user)
      .expect(200)
      .expect((res: any) => {
        const result = JSON.parse(res.text);
        expect(result).toHaveProperty('data');
        expect(result).toHaveProperty('success', true);
        expect(result.data.id).toBe(1);
        expect(result.data.email).toBe(user.email);
        expect(result.data).toHaveProperty('firstName');
        expect(result.data).toHaveProperty('lastName');
        expect(result.data).toHaveProperty('createdAt');
        expect(result.data).toHaveProperty('updatedAt');
      })
      .end(done);
  });
});

  
    // me.route.test.ts
import request, { CallbackHandler } from 'supertest';
import routes from './me.route';
import app from '../../configs/express.config';
import SQLite from '../../configs/sqlite.config';
import userService from '../../services/user/user.service';
import Encryption from '../../utilities/encryption.utility';
import constants from '../../constants';

let token: string;

beforeAll(async () => {
  await SQLite.instance.setup();

  const user = {
    email: 'admin@gmail.com',
    password: 'password',
    firstName: 'First',
    lastName: 'Last',
  };
  await userService.create(user);

  token = await Encryption.generateCookie(constants.COOKIE.KEY_USER_ID, '1');
});

afterAll(() => {
  SQLite.instance.destroy();
});

describe('Testing me router', () => {
  test('Get current user', (done: CallbackHandler) => {
    return request(app.use(routes))
      .get('/me')
      .set('Cookie', `${constants.COOKIE.COOKIE_USER}=${token};`)
      .expect(200)
      .expect((res: any) => {
        const result = JSON.parse(res.text);
        expect(result).toHaveProperty('data');
        expect(result).toHaveProperty('success', true);
        expect(result.data.id).toBe(1);
        expect(result.data).toHaveProperty('email');
        expect(result.data).toHaveProperty('firstName');
        expect(result.data).toHaveProperty('lastName');
        expect(result.data).toHaveProperty('createdAt');
        expect(result.data).toHaveProperty('updatedAt');
      })
      .end(done);
  });

  test('Update current user', (done: CallbackHandler) => {
    return request(app.use(routes))
      .put('/me')
      .set('Cookie', `${constants.COOKIE.COOKIE_USER}=${token};`)
      .send({
        firstName: 'First',
        lastName: 'Last',
      })
      .expect(200)
      .expect((res: any) => {
        const result = JSON.parse(res.text);
        expect(result).toHaveProperty('data');
        expect(result).toHaveProperty('success', true);
        expect(result.data.firstName).toBe('First');
        expect(result.data.lastName).toBe('Last');
      })
      .end(done);
  });
});

  
    // user.route.test.ts
import request, { CallbackHandler } from 'supertest';
import routes from './user.route';
import app from '../../configs/express.config';
import SQLite from '../../configs/sqlite.config';
import userService from '../../services/user/user.service';
import Encryption from '../../utilities/encryption.utility';
import constants from '../../constants';

let token: string;

beforeAll(async () => {
  await SQLite.instance.setup();

  const user = {
    email: 'admin@gmail.com',
    password: 'password',
    firstName: 'First',
    lastName: 'Last',
  };
  await userService.create(user);

  token = await Encryption.generateCookie(constants.COOKIE.KEY_USER_ID, '1');
});

afterAll(() => {
  SQLite.instance.destroy();
});

describe('Testing user router', () => {
  test('Get list users', (done: CallbackHandler) => {
    return request(app.use(routes))
      .get('/user')
      .set('Cookie', `${constants.COOKIE.COOKIE_USER}=${token};`)
      .expect(200)
      .expect((res: any) => {
        const result = JSON.parse(res.text);
        expect(result).toHaveProperty('data');
        expect(result).toHaveProperty('success', true);
        expect(result).toHaveProperty('pagination');
        expect(result.data.length).toBe(1);
      })
      .end(done);
  });

  test('Remove a user', (done: CallbackHandler) => {
    return request(app.use(routes))
      .delete('/user/1')
      .set('Cookie', `${constants.COOKIE.COOKIE_USER}=${token};`)
      .expect(200)
      .expect((res: any) => {
        const result = JSON.parse(res.text);
        expect(result).toHaveProperty('data');
        expect(result).toHaveProperty('success', true);
        expect(result.data.id).toBe(1);
      })
      .end(done);
  });
});

  

Recent Blogs