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.
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
// 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
// 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);
});
});