DEV Community

loading...

NodeJS Boilerplate with Unit Testing - #1

Akhmad Fauzan
Hi, i am a fullstack developer.
Updated on ・4 min read

Introduction

Have we ever made a backend application from scratch. what programming language will be used, what about the performance. is there a lot of community to support it. and several other considerations. I will share a tutorial on how to create a boilerplate for a backend application using Javascript (NodeJS runtime) with Express Framework, how to manage dependencies between layers. and unit testing implementation with chai, sinon and mocha

Pre-requisites!

  • node.js - runtime environment javascript
  • ES6 What is Class, Arrow function in [ECMA SCRIPT 2016]
  • Unit Testing What is unit testing

Folder Structure

.
├── .env
├── .eslintrc.js
├── .gitignore
├── config
   └── database.js
├── package-lock.json
├── package.json
├── src
   ├── MainApplication.js
   ├── common
      ├── middlewares
         ├── index.js
         └── validateSchema.js
      └── repositories
          ├── baseRepository.js
          └── index.js
   ├── index.js
   ├── role
   └── user
       ├── userController.js
       ├── userDependencies.js
       ├── userModel.js
       ├── userRepository.js
       ├── userSchema.js
       └── userService.js
└── test
    ├── MainApplication.test.js
    ├── common
       ├── middlewares
          └── validateSchema.test.js
       └── repositories
           └── baseRepository.test.js
    ├── fixtures
       ├── index.js
       ├── queryRequest.js
       └── userController.js
    ├── setup.js
    └── user
        ├── userController.test.js
        ├── userDependencies.test.js
        └── userService.test.js

13 directories, 28 files

Development

Installation

Iniatiate project

$ npm init

The script will created package.json file. lets install dev dependecies

npm install chai mocha nyc sinon sinon-chai --save-dev

Version of dependencies maybe different with this tutorial. but its does not matter.

{
  "name": "nodejs_platform",
  "version": "0.0.1",
  "description": "NODEJS PLATFORM",
  "main": "index.js",
  "scripts": {
    "start": "node src",
    "lint": "eslint src test",
    "test": "nyc mocha --recursive",
    "test:coverage": "nyc report --reporter=lcov | npm run test"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "joi": "^14.3.1",
    "lodash": "^4.17.15",
    "mysql2": "^2.1.0",
    "p-iteration": "^1.1.8",
    "sequelize": "^5.21.3"
  },
  "devDependencies": {
    "chai": "^4.2.0",
    "eslint": "^6.8.0",
    "eslint-config-airbnb-base": "^14.0.0",
    "eslint-plugin-import": "^2.19.1",
    "mocha": "^6.2.2",
    "nyc": "^15.0.0",
    "sinon": "^8.0.0",
    "sinon-chai": "^3.3.0"
  },
  "mocha": {
    "globals": "global",
    "require": "test/setup.js"
  },
  "nyc": {
    "all": true,
    "include": [
      "src/**/*.js"
    ],
    "exclude": [
      "src/index.js"
    ]
  }
}

Unit Testing

create user.service.test file inside test/user/. see folder structure

const sinon = require('sinon');

const UserService = require('../../src/user/userService');

describe('User Service', () => {
  const sandbox = sinon.createSandbox();
  let props;
  let userService;

  beforeEach(() => {
    props = {
      userRepository: {
        get: sandbox.stub(),
        filterAndSort: sandbox.stub(),
      },
    };

    userService = new UserService(props);
  });

  afterEach(() => {
    sandbox.restore();
  });

  describe('#create', () => {
    it('should return user when create user is successfully', () => {
      const expectedUser = {
        userName: 'bob',
        email: 'bob@sit.com',
      };

      const result = userService.create();

      expect(result).to.deep.equal(expectedUser);
    });
  });

  describe('#getById', () => {
    it('should call get userRepository with expected params', async () => {
      const id = 'USR001';
      const expectedParam = {
        id,
      };

      await userService.getById(id);

      expect(props.userRepository.get).to.calledWith(expectedParam);
    });
  });

  describe('#filterAndSort', () => {
    it('should call filterAndSort userRepository with expected params', async () => {
      const expectedParam = {
        filters: [
          {
            key: 'email',
            operator: 'LIKE',
            value: '@gmail',
          },
        ],
        sorts: [
          {
            key: 'email',
            order: 'DESC',
          },
        ],
        page: {
          limit: 10,
          offset: 0,
        },
      };

      await userService.filterAndSort(expectedParam);

      expect(props.userRepository.filterAndSort).to.calledWith(expectedParam);
    });
  });
});

Unit Testing Description

Set up dependency module for unit test

const { expect, use } = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');

const UserService = require('../../src/user/user.service');

use(sinonChai);

Chai : a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework
Sinon : Standalone and test framework agnostic JavaScript test spies, stubs and mocks
Sinon-Chai : provides a set of custom assertions for using the Sinon.JS like "to.have.been.calledWith"

Test Setup environment

const sandbox = sinon.createSandbox();
  let props;
  let userService;

  beforeEach(() => {
    props = {
      userRepository: {
        get: sandbox.stub(),
      },
    };

    userService = new UserService(props);
  });

  afterEach(() => {
    sandbox.restore();
  });

beforeEach : runs before every Runnable instance;
props : properties needed for user service
sandbox.stub() : Override an object’s property for the test and mocking result function
sandbox.restore() : Restores all fakes created through sandbox.
afterEach: runs after every Runnable instance;

Create test case #1

describe('#create', () => {
    it('should return user when create user is successfully', () => {
      const expectedUser = {
        userName: 'bob',
        email: 'bob@sit.com',
      };                                            // Arrange

      const result = userService.create();          // Action

      expect(result).to.deep.equal(expectedUser);   // Assert
    });
  });

Unit test anatomy [AAA]:
Arrange : everything we need to run the experiment. you may need to seed an object with some variable values or init callback function
Action : represents the star of the unit testing show. we invoke the create method and capture the result.
Assert : something represents the essence of testing.

Better One Assert Per Test Method

Create test case #2

describe('#getById', () => {
    it('should call get userRepository with expected params', async () => {
      const id = 'userId';
      const expectedParam = {
        id,
      };

      await userService.getById(id);

      expect(props.userRepository.get).to.calledWith(expectedParam);
    });
  });

unit testing will be on every layer. for getById we only need to make sure that this function calls get userRepository with the correct parameter

Keep Unit Test Short, Sweet, and Visible

Create Service

Create user service inside src/user/. see folder structure

class UserService {
  constructor(props) {
    Object.assign(this, props);
  }

  async getById(id) {
    const params = {
      id,
    };

    return this.userRepository.get(params);
  }

  async filterAndSort(queryRequest) {
    return this.userRepository.filterAndSort(queryRequest);
  }

  create() {
    return {
      userName: 'bob',
      email: 'bob@sit.com',
    };
  }
}

module.exports = UserService;

Run Unit Test

mocha test --recursive

Discussion (3)

Collapse
slimcoder profile image
Slim Coder

Hey! What is name of this boilerplate? It is your custom boilerplate?

Collapse
wiratamap profile image
Wiratama Paramasatya

This is very nice article. Thanks for share!

Some comments have been hidden by the post's author - find out more