Testing everything against the real database in Nodejs (Typescript) application by integrating unit test and integration test with Jest.
By: Phat Vo
Tags: Continous Integration, Nodejs, Unit Testing, Typescript, Jest

Testing everything against the real database in Nodejs (Typescript) application by integrating unit test and integration test with Jest. Phat Vo 4 days ago·18 min read

In the world of software development’s hunger for authenticity, we can conceive the importance of the testing mechanism. The cycle of an application operation always relies on the meticulousness of the development process and testing everything in your application is a guarantee for stability things.

From the monolithic to microservices, the testing pulls out all the errors. Unit tests make it safer and easier to refactor the code by putting tests into place that make sure refactoring occurs without problems and disruption. It takes the risk out of changing older source code. It helps find problems and resolve them before further testing so they won’t impact other bits of code.

Hence in this article, we will learn more about the Unit test, Integration test, and how to apply them to a Nodejs (Typescript) application. We will integrate the test against the real database and run the test in CI (Continuous integration) with Github Actions.

Before going to integrate the tests. I have a short brief what’s the unit test and integration test?

What’s unit testing?

Unit Testing is one of the many stages of software testing and looks at single units, otherwise known as components, individually. This validates that each component of the software being tested works as it is designed to. Each unit is the smallest functional part of the software that can be tested and normally only has one input and one output.

What is Integration Testing?

Integration testing is the process of testing the interface between two software units or modules. Its focus is on determining the correctness of the interface. The purpose of integration testing is to expose faults in the interaction between integrated units. Once all the modules have been unit tested, integration testing is performed.

Now, let’s going to integrate the unit test and integration test in Nodejs (Typescript) application. This tutorial requires you have some basic knowledge in Typescript, Jest (a testing framework).

Actually, I have a repository that is situated in Github. That’s a demo NodeJS (Typescript) application called Task Management with fully testing integrated. Hence, I’ll pick some testing parts in that repository to explain here in the tutorial.

You can have an overview and fully introduction of this application via the attached link here: https://github.com/phatvo21/task-management

In the application mentioned above, I’ve implemented the unit test and integration test against a real Mysql database. So, let’s see how’s it works.

Before integrating the tests, we have to install all kinds of essential dependencies:

Install Jest, that’s a Javascript testing framework: https://www.npmjs.com/package/jest

Install Ts-Jest: https://www.npmjs.com/package/ts-jest

Install Suppertest: https://www.npmjs.com/package/supertest

Install Factory Girl, this library supports associations and the use of functions for generating attributes, data for Database Model: https://www.npmjs.com/package/factory-girl

Install Faker, this library generates massive amounts of fake data: https://www.npmjs.com/package/faker

Install Ioredis Mock, because the application using Redis for caching mechanism: https://www.npmjs.com/package/ioredis-mock

Install husky, we need to perform all the tests before any code pushing: https://www.npmjs.com/package/husky

After installing all the dependencies, we are going to config the Jest testing framework in our root directory of the application.

Create a jest.config.js file in the root directory and make some config as below, I’ve left the comment to explains the responsibility of every single line in the config file.

module.exports = {

// A preset that is used as a base for Jest's configuration

preset: 'ts-jest',

// This will be used to configure minimum threshold enforcement for coverage results

// With the following configuration jest will fail if there is less than around 80% branch,

// line, and function coverage

coverageThreshold: {

global: {

functions: 79,

lines: 80,

statements: 80,

},

},

// Default timeout of a test in milliseconds.

testTimeout: 10000,

// An array of file extensions your modules use.

// If you require modules without specifying a file extension,

// these are the extensions Jest will look for, in left-to-right order

moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'],

// Indicates whether the coverage information should be collected while executing the test

collectCoverage: true,

// An array of glob patterns indicating a set of files for which coverage information should be collected

collectCoverageFrom: [

'**/**/*.ts',

'!**/node_modules/**',

'!**/build/**',

'!**/coverage/**',

'!dist/**',

'!jest.config.js',

'!**/test/**',

'!test/**',

'!**/src/databases/migrations/**',

],

// Indicates the code transformation into javascript when the test running

transform: {

'^.+\\.(ts|tsx)$': 'ts-jest',

},

// A list of paths to modules that run some code to configure or set up the testing framework before each test file in the suite is executed

// In this case indicates that the environment configuration will be executed before each test

setupFiles: ['<rootDir>/test/env.ts'],

// A list of reporter names that Jest uses when writing coverage reports

coverageReporters: ['text', 'text-summary'],

// The pattern or patterns Jest uses to detect test files

// By default it looks for .js, .jsx, .ts and .tsx as well as any files with a suffix of .test or .spec

testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)x?$',

// An array of regexp pattern strings that are matched against all test paths before executing the test.

// If the test path matches any of the patterns, it will be skipped.

testPathIgnorePatterns: ['/node_modules/', '/build/', '/coverage/', '/dist/'],

// A map from regular expressions to module names or to arrays of module names that allow to stub out resources,

// like images or styles with a single module.

moduleNameMapper: {

'^services/(.*)': '<rootDir>src/services/$1',

'^components/(.*)': '<rootDir>src/components/$1',

'^helpers/(.*)': '<rootDir>src/helpers/$1',

'^middlewares/(.*)': '<rootDir>src/middlewares/$1',

'^utils/(.*)': '<rootDir>src/utils/$1',

'^logging/(.*)': '<rootDir>src/logging/$1',

'^databases/(.*)': '<rootDir>src/databases/$1',

'^repositories/(.*)': '<rootDir>src/repositories/$1',

'^constants/(.*)': '<rootDir>src/constants/$1',

'^consoles/(.*)': '<rootDir>src/consoles/$1',

},

};

Secondly, we are going to create a folder called test and define the necessary testing things into that folder. The folder is located in the root directory.

test folder in the application

Let’s go through the details of each file, directory in this test folder.

env.ts this file contains all the environment’s variables. We have to pass the real Mysql database, Redis credentials, and some other needed information into this file.

import * as faker from 'faker';

/**

* Load a test environment

*/

process.env.SILENCE_REPORT = 'true';

process.env.PORT = faker.datatype.number(1000, 9000);

process.env.NODE_ENV = 'testing';

process.env.SERVER_TIMEOUT = '1080000';

/**

* SQL

*/

process.env.SQL_HOST = process.env.SQL_HOST ?? '127.0.0.1';

process.env.SQL_USER = process.env.SQL_USER ?? 'root';

process.env.SQL_PASSWORD = process.env.SQL_PASSWORD ?? '';

process.env.SQL_DB = process.env.SQL_DB ?? 'task_management';

/**

* Redis

*/

process.env.REDIS_PORT = '6379';

process.env.REDIS_HOST = process.env.REDIS_HOST ?? '127.0.0.1';

process.env.REDIS_PASSWORD = process.env.REDIS_PASSWORD ?? '';

process.env.REDIS_DB = '1';



/**

* JWT

*/

process.env.JWT_SECRET = 'superlongsecretkeygoeshereprobablyagoodideatochangethisperproject';

process.env.JWT_TTL = '4h';

process.env.JWT_KEY = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';

process.env.JWT_ENCRYPTION_KEY = 'x!A%D*G-KaPdSgVkYp3s6v8y/B?E(H+MbQeThWmZq4t7w!z$C&F)J@NcRfUjXn2r';



/**

* HMAC

*/

process.env.HTTP_HMAC_PRIVATE_KEY = 'thisisprivatekey';

process.env.HTTP_HMAC_PUBLIC_KEY = 'thisispublickey';

hooks.ts This hook design is the heart of our testing mechanism. The hook will run before every single unit test and being charge of start server, run database migration, revert the database migration, init all the test mock/stub, recreate or clear the database (Redis, SQL). Each functionality performs before all and after all tests.

import * as Server from '../src/server';

import {getManager} from 'typeorm';

import * as IORedis from 'ioredis';

import {CacheCredentials} from '../src/utils/cache-storage/CacheCredentials';

import {config} from '../src/constants/Config';

import {runMigration} from '../src/databases/RunMigration';



let entityManager = getManager();



/**

* Global test hooks

*/

export function hooks() {

let app;



beforeAll(async () => {

// Init the server

app = await Server.default;

// Recreate the database

await reCreateDB();

// Run the migration

await runMigration();

});



afterAll(async () => {

// Clear all test mocks

jest.resetAllMocks();

// Clear all test modules

jest.resetModules();

// Clear all test data in the database

await clearDB();

// Clear all the data in cache

const parsed = new CacheCredentials(config.redis.url);

const ioRedis = new IORedis({...parsed});

await ioRedis.flushdb();

});

}



export async function reCreateDB() {

await entityManager.transaction(async (transactionalEntityManager) => {

return await transactionalEntityManager

.query(`DROP DATABASE IF EXISTS ${process.env.SQL_DB}`)

.then(async function () {

return await transactionalEntityManager.query(

`CREATE DATABASE ${process.env.SQL_DB} CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci`,

);

})

.then(async function () {

return await transactionalEntityManager.query(`USE ${process.env.SQL_DB}`);

});

});

}



export async function clearDB() {

await entityManager.transaction(async (transactionalEntityManager) => {

const tables = await transactionalEntityManager.query(

`SELECT table_name FROM information_schema.tables WHERE table_schema="${process.env.SQL_DB}";`,

);

for (let i = 0; i < tables.length; i++) {

const table = tables[i]['table_name'];

if (table !== 'migrations') {

await transactionalEntityManager.query(`SET FOREIGN_KEY_CHECKS = 0;`);

await transactionalEntityManager.query(`TRUNCATE TABLE ${table};`);

await transactionalEntityManager.query(`SET FOREIGN_KEY_CHECKS = 1;`);

}

}

});

}

interceptor.ts This design the mocks of request, response, and next function in Express middleware.

export const interceptor = {

mockRequest: () => {

const req: any = {};

req.body = jest.fn().mockReturnValue(req);

req.params = jest.fn().mockReturnValue(req);

req.app = jest.fn().mockReturnValue(req);

req.app.get = jest.fn().mockReturnValue(req);

return req;

},

mockResponse: () => {

const res: any = {};

res.send = jest.fn().mockReturnValue(res);

res.status = jest.fn().mockReturnValue(res);

res.json = jest.fn().mockReturnValue(res);

res.locals = jest.fn().mockReturnValue(res);

return res;

},

mockNext: () => jest.fn(),

};

Inside folder __mocks__/main.ts design the Redis Mock mechanism using ioredis-mock library.

import * as Ioredis from 'ioredis-mock';

import {Redis, KeyType, ValueType} from 'ioredis';



export class MockCache {

cache: Redis;

constructor(data: any) {

this.cache = new Ioredis({data}) as Redis;

}

async set(key: KeyType, value: ValueType, expiryMode: string, time: number) {

return this.cache.pipeline().set(key, value, expiryMode, time).exec();

}

async get(key: KeyType) {

return this.cache.pipeline().get(key).exec();

}

async del(key: KeyType) {

return this.cache.pipeline().del(key).exec();

}

async keyExists(key) {

return this.cache.pipeline().exists(key).exec();

}

async keys(pattern) {

return this.cache.pipeline().keys(pattern).exec();

}

}

To mock all the data tests against the real database requires us to design a mechanism and the current application has one called factory.

The database simply has two tables, which are the users and tasks table. I have used TypeOrm (That’s an ORM) and define all the database models based on this ORM. Hence, we need the factory-girl library to support associations and the use of functions for generating attributes, data of database tables.

Factory

factory/workers/user This is a user factory worker, it defines all the fields and generates fakes data matches to Users Model.

import * as faker from 'faker';

import {Users} from '../../../src/databases/entities/Users';



export function userFactoryWorker(overrides): Users {

return {

email: faker.internet.email(),

password: faker.internet.password(),

name: faker.lorem.word(),

createdAt: faker.date.recent(),

updatedAt: faker.date.recent(),

...overrides,

};

}

factory/workers/task This is a task factory worker, it defines all the fields and generates fakes data matches to Tasks Model.

import * as faker from 'faker';

import {Tasks} from '../../../src/databases/entities/Tasks';



export function taskFactoryWorker(overrides): Tasks {

return {

title: faker.lorem.word(),

description: faker.lorem.word(),

taskIdentity: faker.lorem.word(),

status: 'pending',

createdAt: faker.date.recent(),

updatedAt: faker.date.recent(),

...overrides,

};

}

factory/adapter.ts Register the specific database model in a factory

import {DefaultAdapter} from 'factory-girl';

import {getRepository} from 'typeorm';



/**

* Adapters provide support for different databases and ORMs.

* Adapters can be registered for specific models, or as the 'default adapter',

* which is used for any models for which an adapter has not been specified

*/

export class TypeOrmAdapter extends DefaultAdapter {

build(Model, props): Promise<any> {

const entityManager = getRepository(Model);

return entityManager.save(props);

}

}

factory/task.ts Defines all the task schematics for a factory

// @ts-ignore

import {taskFactoryWorker} from './workers/task';

import {Tasks} from 'databases/entities/Tasks';



/**

* Defines all the task schematics for a factory

* @param {object} factory - Factory-Girl factory object

*/

export function defineTaskFactory(factory) {

factory.define('task', Tasks, taskFactoryWorker);

}

factory/user.ts Defines all the user schematics for a factory

// @ts-ignore

import {userFactoryWorker} from './workers/user';

import {Users} from 'databases/entities/Users';



/**

* Defines all the user schematics for a factory

* @param {object} factory - Factory-Girl factory object

*/

export function defineUserFactory(factory) {

factory.define('user', Users, userFactoryWorker);

}

And finally inside the factory we can generate and defines a factory object that can create all of our data models.

import {factory} from 'factory-girl';

// @ts-ignore

import {TypeOrmAdapter} from './adapter';

// @ts-ignore

import {defineUserFactory} from './user';

// @ts-ignore

import {defineTaskFactory} from './task';



/**

* Generates and defines a factory object that can create all of our data models

*/



factory.setAdapter(new TypeOrmAdapter());



defineUserFactory(factory);



defineTaskFactory(factory);



export {factory};

The usage of the factory is simply to create user or task data inside the database table. The following methods show the usage of this factory:

await facory.build('user')

await facory.build('task')

In the package.json we need to define an npm command for the test:

"test": "NODE_ENV=testing jest --no-cache --coverage --silent --passWithNoTests --detectOpenHandles --forceExit"

We have configured all the necessary things for our test. Now, let’s going to integrate the unit test and integration test for the simple Controller, Service, and endpoints.

Unit Test

The application has the fundamental functionality that allows the user to register, log in and create a task manager after login. All the features, functionalities have been compressed inside the components folder into the src directory. So, let’s going to implement the Unit Test for AuthController and AuthService, that controller and service represents user’s register and login functionality.

Auth component

AuthController.test.ts This performs the unit test for register and login function in AuthController.ts

Please look through the file, I have left some comments there that explain exactly every single function in this Unit Test.

import 'reflect-metadata';

import {AuthController} from './Auth.Controller';

import {AuthService} from './AuthService';

import {UserRepository} from 'repositories/UserRepository';

// @ts-ignore

import {interceptor} from '../../../test/interceptor';

import * as faker from 'faker';

// @ts-ignore

import {factory} from '../../../test/factory/factory';

// @ts-ignore

import {hooks} from '../../../test/hooks';

import {UserModel} from './models/UserModel';



// Init Global hook before the test

hooks();



// Init mock request and response from interceptor

const {mockRequest, mockResponse} = interceptor;



// Mock the AuthService class

jest.mock('components/auth/AuthService', () => {

const mAuthService = {

register: jest.fn(),

login: jest.fn(),

findUserByEmail: jest.fn(),

};

return {AuthService: jest.fn(() => mAuthService)};

});



// Mock the UserRepository class

jest.mock('repositories/UserRepository', () => {

const mUserRepository = {

create: jest.fn(),

findByCondition: jest.fn(),

};

return {UserRepository: jest.fn(() => mUserRepository)};

});



describe('AuthController', () => {

let userRepository: UserRepository;

let authService: AuthService;

let authController: AuthController;

let req: any;

let res: any;

let token;

const email = faker.internet.email();

const password = faker.internet.password();

const name = faker.lorem.word();

let user;



// Prepare user's credentials before registering

const createBody: UserModel = {

name,

email,

password,

};



// Prepare user's credentials before login

const loginBody: UserModel = {

email,

password,

};



// Build and mock the test data before the testing

beforeAll(async () => {

// Create a user in user database table using factory

user = await factory.build('user', {email, password, name});



// Assignee the defined variables

req = mockRequest();

res = mockResponse();

token = faker.datatype.uuid();



// Init UserRepository instance

userRepository = new UserRepository();



// Init AuthService instance

authService = new AuthService(userRepository);



// Mock values return for register method inside AuthService class

(authService.register as jest.MockedFunction<any>).mockReturnValue({data: user, status: 201});



// Mock values return for login method inside AuthService class

(authService.login as jest.MockedFunction<any>).mockReturnValue({

data: {

token,

status: true,

},

status: 200,

});



// Init AuthController instance

authController = new AuthController(authService);

});



// Performs the user's register test

it('should register a new user', async () => {

(authService.findUserByEmail as jest.MockedFunction<any>).mockReturnValue();

const result = await authController.userRegister(res, req, createBody);

expect(authService.findUserByEmail).toHaveBeenCalledTimes(1);

expect(authService.findUserByEmail).toBeCalledWith(createBody.email);

expect(authService.register).toHaveBeenCalledTimes(1);

expect(authService.register).toBeCalledWith(createBody);

expect(result.status).toBeCalledWith(201);

});



// Performs the user's login test

it("should login using user's credentials", async () => {

(authService.findUserByEmail as jest.MockedFunction<any>).mockReturnValue(user);

const result = await authController.userLogin(res, req, loginBody);

expect(authService.findUserByEmail).toHaveBeenCalledTimes(1);

expect(authService.findUserByEmail).toBeCalledWith(loginBody.email);

expect(result.status).toBeCalledWith(200);

});

});

AuthService.test.ts This performs the unit test for register and login function in AuthService.ts

Please look through the file, I have left some comments there that explain exactly every single function in this Unit Test.

import 'reflect-metadata';

import {AuthService} from './AuthService';

import {UserRepository} from 'repositories/UserRepository';

import * as faker from 'faker';

// @ts-ignore

import {factory} from '../../../test/factory/factory';

// @ts-ignore

import {hooks} from '../../../test/hooks';

import {UserModel} from './models/UserModel';

import * as helpers from 'helpers/Helpers';

import {genSaltSync, hashSync} from 'bcrypt';



// Init Global hook before the test

hooks();



// Mock the UserRepository class

jest.mock('repositories/UserRepository', () => {

const mUserRepository = {

create: jest.fn(),

findByCondition: jest.fn(),

};

return {UserRepository: jest.fn(() => mUserRepository)};

});



describe('AuthService', () => {

let userRepository: UserRepository;

let authService: AuthService;

const email = faker.internet.email();

const password = faker.internet.password();

const name = faker.lorem.word();

const createdAt = faker.date.recent();

let user;

const token = faker.datatype.uuid();

let currentDateTimeMock;

let generateAccessMock;

let hash = hashSync(password, genSaltSync(10));



// Prepare user's credentials before creating

const createBody: UserModel = {

name,

email,

password,

};



// Prepare user's credentials before login

const loginBody: UserModel = {

email,

password,

};



// Build and mock the test data before the testing

beforeAll(async () => {

// Create a user in users database table using factory

user = await factory.build('user', {email, password: hash, name, createdAt, updatedAt: createdAt});



// Init UserRepository instance

userRepository = new UserRepository();



// Init AuthService instance

authService = new AuthService(userRepository);



// Mock values return for create method inside UserRepository class

userRepository.create = jest.fn().mockReturnValue(user);



// Mock values return for findByCondition method inside UserRepository class

userRepository.findByCondition = jest.fn().mockReturnValue({

id: user.id,

email,

password: hash,

name,

createdAt,

updatedAt: createdAt,

});



// Mock values return for currentDateTime function inside helpers

currentDateTimeMock = jest.spyOn(helpers, 'currentDateTime').mockReturnValue(createdAt);



// Mock values return for generateAccessToken function inside helpers

generateAccessMock = jest.spyOn(helpers, 'generateAccessToken').mockReturnValue(token);

});



// Performs create user test

it('should register a new user', async () => {

const {

data: {email, password, name},

status,

} = await authService.register(createBody);

expect(userRepository.create).toHaveBeenCalledTimes(1);

expect(currentDateTimeMock).toHaveBeenCalledTimes(1);

expect(status).toEqual(201);

expect(email).toEqual(user.email);

expect(password).toEqual(user.password);

expect(name).toEqual(user.name);

});



// Performs user's login using credentials test

it("should login using user's credentials", async () => {

const {data, status} = await authService.login(loginBody);

expect(userRepository.findByCondition).toHaveBeenCalledTimes(1);

expect(userRepository.findByCondition).toBeCalledWith({email: loginBody.email});

expect(generateAccessMock).toHaveBeenCalledTimes(1);

expect(generateAccessMock).toBeCalledWith({id: user.id, email: user.email});

expect(status).toEqual(200);

expect(data.token).toEqual(token);

});

});

Integration Test

An integration test is more like testing the API endpoint. In our case, we need to test the register and login endpoints. The application requires HMAC signature authentication for all of the endpoints, hence we need to integrate a test mechanism for this HMAC signature.

In the test/stub directory, let’s create a file called getHmacSignature.ts and define a function that allows generating an HMAC key for testing.

import {hmacEncrypt} from '../../src/helpers/Helpers';

import {httpHmacPrivateKey, httpHmacPublicKey} from '../../src/constants/Config';



export function getHmacSignature(): string {

return hmacEncrypt(httpHmacPrivateKey(), httpHmacPublicKey());

}

In the test/integration/auth directory let’s create two files called login-api.test.ts and register-api.test.ts . And, performs the integration test inside those files.

Please have a look thoroughly into these files, I’ve left some comments to explain every single function using in those integration tests.

login-api.test.ts This testing the HTTP request to login API.

import * as request from 'supertest';

import {createApp} from '../../../src/app';

// @ts-ignore

import {getHmacSignature} from '../../stub/getHmacSignature';

import * as faker from 'faker';

// @ts-ignore

import {factory} from '../../../test/factory/factory';

import {genSaltSync, hashSync} from 'bcrypt';

// @ts-ignore

import {clearDB, reCreateDB} from '../../hooks';

import {runMigration} from '../../../src/databases/RunMigration';



let app;

// Init application and database before testing

beforeAll(async () => {

// Init App

app = await createApp();

// Recreate the database

await reCreateDB();

// Run the database migration

await runMigration();

});



// Clear the database after the test

afterAll(async () => {

// Clear the database

await clearDB();

});



// Login endpoint

const apiUrl = '/api/v1/auth/login';



describe('POST - auth/login', () => {

let body;

let user;

// User credentials

const password = faker.internet.password();

const email = faker.internet.email();

const name = faker.lorem.word();

const hash = hashSync(password, genSaltSync(10));



beforeAll(async () => {

// Create a user before all the tests

user = await factory.build('user', {email, password: hash, name});

});



beforeEach(async () => {

// Prepare the request body

body = {

password,

email,

};

});



it('should return error invalid hmac', async (done) => {

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: 'invalid',

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(401);

expect(res.body.message).toMatch(/The hmac signature does not match/);

return done();

});

});



it('should return error invalid email', async (done) => {

body.email = faker.lorem.word();

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(400);

expect(res.body.message[0].constraints.isEmail).toMatch(/email must be an email/);

return done();

});

});



it('should return error empty password', async (done) => {

delete body.password;

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(400);

expect(res.body.message[0].constraints.isNotEmpty).toMatch(/password should not be empty/);

return done();

});

});



it('should return error empty email', async (done) => {

delete body.email;

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(400);

expect(res.body.message[0].constraints.isNotEmpty).toMatch(/email should not be empty/);

return done();

});

});



it('should return error if invalid credentials', async (done) => {

body.password = 'thiIsInvalidPassword';



request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(401);

expect(res.body.data.message).toMatch(/The username or password dose not match./);

return done();

});

});



it('should login successfully', async (done) => {

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(200);

expect(res.body.data.token).toBeDefined();

expect(res.body.data.status).toEqual(true);

return done();

});

});

});

register-api.test.ts This testing the HTTP request to register API.

import * as request from 'supertest';

import {createApp} from '../../../src/app';

// @ts-ignore

import {getHmacSignature} from '../../stub/getHmacSignature';

import * as faker from 'faker';

// @ts-ignore

import {factory} from '../../../test/factory/factory';

// @ts-ignore

import {clearDB, reCreateDB} from '../../hooks';

import {runMigration} from '../../../src/databases/RunMigration';



let app;

// Init application and database before testing

beforeAll(async () => {

// Init App

app = await createApp();

// Recreate the database

await reCreateDB();

// Run the database migration

await runMigration();

});



// Clear the database after the test

afterAll(async () => {

// Clear the database

await clearDB();

});



// Login endpoint

const apiUrl = '/api/v1/auth/register';



describe('POST - auth/register', () => {

let body;

beforeEach(async () => {

// Prepare the request body for register

body = {

name: faker.lorem.word(),

password: faker.internet.password(),

email: faker.internet.email(),

};

});



it('should return error invalid hmac', async (done) => {

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: 'invalid',

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(401);

expect(res.body.message).toMatch(/The hmac signature does not match/);

return done();

});

});



it('should return error invalid email', async (done) => {

body.email = faker.lorem.word();

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(400);

expect(res.body.message[0].constraints.isEmail).toMatch(/email must be an email/);

return done();

});

});



it('should return error empty password', async (done) => {

delete body.password;

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(400);

expect(res.body.message[0].constraints.isNotEmpty).toMatch(/password should not be empty/);

return done();

});

});



it('should return error empty email', async (done) => {

delete body.email;

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(400);

expect(res.body.message[0].constraints.isNotEmpty).toMatch(/email should not be empty/);

return done();

});

});



it('should return error if email already exist', async (done) => {

await factory.build('user', {email: body.email, password: body.password, name: body.name});

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(409);

expect(res.body.message).toMatch(/This email already exist!/);

return done();

});

});



it('should create a new user', async (done) => {

request(app)

.post(apiUrl)

.set({

HTTP_HMAC: getHmacSignature(),

})

.send(body)

.end((err, res) => {

if (err) return done(err);

expect(res.status).toBe(201);

expect(res.body.data.id).toBeDefined();

expect(res.body.data.name).toEqual(body.name);

expect(res.body.data.email).toEqual(body.email.toLowerCase());

return done();

});

});

});

Finally, we can run all the tests using the command npm run test . The results would be worked like charm as shows as below:

npm run test

Run test in Docker environment

Some developers tend to gravitate towards Docker rather than using every single environment installed on your machine. Not only does using the Docker environment run the test we have implemented but also serves the purpose of running the testing on CI (Continuous Integration).

Let’s going to config Docker and CI (Github Actions) in the application

Create Dockerfile file in the root directory of the application

# Instal package dependencies

FROM node:12-alpine AS dependency



# Defind the working directory

WORKDIR /task-management



# Copy the packages file to root directory in docker container

COPY package*.json ./

RUN npm ci



# Build source

FROM dependency AS base

COPY . .



# Build source

FROM base AS build

RUN npm run build



# Ship compiled sources

FROM dependency



COPY --from=build /task-management/dist ./dist



RUN npm prune --production



# Start run the application

CMD ["node", "./dist/server.js"]

Create docker-compose.yaml file in the root directory of the application

# Docker compose files used for testing in container

version: '3'



services:

service:

build:

context: .

# stop at the dependency stage

target: base

# override the command to run the test functions

command: ['run', 'test:ci']

entrypoint: npm

depends_on:

- mysql

- redis

environment:

SQL_HOST: mysql

SQL_PASSWORD: root

REDIS_HOST: redis



# Mysql image

mysql:

image: mysql:5.7

environment:

MYSQL_ROOT_PASSWORD: root

MYSQL_DATABASE: task_management

expose:

- '3306'



# Mysql image

redis:

image: redis:6-alpine

expose:

- '6379'

And, let’s declare the proper commands to run the tests in the Docker environment. The commands must be defined in package.json file

"test:ci": "npm run test",

"test:compose": "docker-compose up --abort-on-container-exit --build"

We then can run the tests in Docker environment via the command npm run test:compose

CI using Github Actions

When testing is a key component of your continuous integration and continuous delivery (CI/CD) implementation, you will be able to deliver with speed and reliability. When configured, your CI/CD pipeline will automatically build, test, and deploy your code when it is merged into a branch.

In our case, when the code is pushed or pull a request against a main branch the testing is required in CI. Meanwhile, the test has to run in CI pieces of stuff we also require those tests to run in the local before pushing any change to remove.

Using husky to run the test before pushing changes. The config of husky situated in package.json like below.

"husky": {

"hooks": {

"pre-push": "npm test"

}

},

Config Github worksflow

Create the config.yaml file which is located in the folder .github/workflows . The config.yaml contains the following information as shown below.

# This is a basic workflow to help you get started with Actions

name: CI



# Controls when the action will run. Triggers the workflow on push or pull request

# events but only for the master branch

on:

push:

branches: [main]

pull_request:

branches: [main]



jobs:

build:

timeout-minutes: 10

runs-on: ubuntu-latest



strategy:

matrix:

node-version: [12.x]



steps:

- name: Checkout

uses: actions/checkout@v1



- name: Run tests

run: npm run test:compose



- name: Stop containers

if: always()

run: docker-compose -f "docker-compose.yml" down

Finally, we can push all the Unit testing and Integration testing to the remote and those tests will perform in the CI.

After pushing all changes, we can be landing on Github’s repository and click on Actions . There will be a pipeline running and performs the test as our local.

The test running in CI pipeline

The test run on the Docker container competed in the CI pipeline

That’s it. We have integrated the unit test and integration test against the real database that works like a charm in both environments, not only on the local machine and Docker environment but also in the CI pipeline.

In the earlier, I’ve mentioned Redis and ioredis-mock . But throughout the process, we don’t have time to flick through the details of them. Hence, I highly recommend you have a look at the existing application in Github, which is called Task Management.

Github Repo: https://github.com/phatvo21/task-management