A good software development project should have the following* at the very basic:

  • Linting - to enforce coding convention
  • Testing - to ensure your code actually works and new changes don't break working code
  • Test coverage - to have an insight about how much of your code is tested
  • Continuous Integration (CI) - for pulling in frequent changes to the main branch

In this tutorial, I will show you how to set up the combo of linting, testing, test coverage, and CI for a Node.js project using ESLint, Mocha, Istanbul, Codecov, and CircleCI.

I prefer CircleCI over Travis because jobs on Travis take a really long time to run; and I prefer Codecov over Coveralls because Codecov is more free (as in lunch) than Coveralls.

* Version control is a given.

Setting up ESLint, Mocha, Istanbul, Codecov, and CircleCI for Node.js projects#

Start by creating a directory named arithmetic and run npm init in it.

$ mkdir arithmetic
$ cd arithmetic
$ npm init

This will create a package.json file with the following content:

package.json
{
  "name": "arithmetic",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Create a file named index.js with the following content:

index.js
exports.add = add;
exports.subtract = subtract;

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}
1
2
3
4
5
6
7
8
9
10

A revolutionary arithmetic library right there! Time to write tests for this stupendous library.

Linting with ESLint#

Install eslint as a development dependency:

$ npm i -D eslint

Create a .eslintignore file to exclude paths from being lint.

.eslintignore
coverage
node_modules

Now create a .eslintrc.yml to configure eslint for the project. Feel free to customize it.

.eslintrc.yml
root: true

env:
  node: true

parserOptions:
  ecmaVersion: 6

rules:
  eol-last: error
  eqeqeq: [error, allow-null]
  indent: [error, 2, { SwitchCase: 1 }]
  no-trailing-spaces: error
  no-unused-vars: [error, { vars: all, args: none, ignoreRestSiblings: true }]

Then add a npm script named lint to run eslint on the repository.

package.json
{
  "name": "arithmetic",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "lint": "eslint ."
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "eslint": "^6.6.0"
  },
}

Confirm linting is set up right and there are no linting errors.

$ npm run lint

Testing with Mocha#

Mocha is the testing framework, Chai is the assertion library. It is not mandatory to use Chai with Mocha, you could even use Node's in-built assert module, Chai just provides a very friendly interface for writing your assertions.

Install Mocha and Chai as development dependencies.

$ npm i -D mocha chai

Create a file named test.js and write the tests.

test.js
const expect = require('chai').expect;
const lib = require('./index');
const add = lib.add;
const subtract = lib.subtract;

describe('arithmetic functions', () => {
  it('add() should add', () => {
    expect(add(2, 2)).to.equal(4);
  });

  it('subtract() should subtract', () => {
    expect(subtract(2, 2)).to.equal(0);
  });
});

Add a test script in the package.json file.

package.json
{
  "name": "arithmetic",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "lint": "eslint .",
    "test": "mocha test"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "^4.2.0",
    "eslint": "^6.6.0",
    "mocha": "^6.2.2"
  }
}

Now run npm test.

$ npm test

> arithmetic@1.0.0 test /projects/arithmetic
> mocha test

  arithmetic functions
    ✓ add() should add
    ✓ subtract() should subtract

  2 passing (6ms)

All tests passing. No linting problems either!

Initialize the directory as a git repository, commit the changes and push to master.

$ git init
$ git remote add origin git@github.com:hacksparrow/arithmetic.git # Add your repo address
$ echo node_modules > .gitignore
$ git add .
$ git commit -m 'init'
$ git push origin master

Test coverage with Istanbul#

We will now create a new branch named dev-tools and start making some changes to the repository.

$ git checkout -b dev-tools

Update index.js to make it smarter (detect missing parameters).

test.js
exports.add = add;
exports.subtract = subtract;

function add(a, b) {
  if (!a || !b) {
    throw new Error('Missing arguments');
  } else {
    return a + b;
  }
}

function subtract(a, b) {
  if (!a || !b) {
    throw new Error('Missing arguments');
  } else {
    return a - b;
  }
}

Some test coverage tools may not be able to see statements on the samle line as a condition:

if (condition) return 'foo'; // Invisible to test coverage
return 'bar';

therefore, always use curly brackets:

if (condition) {
  return 'foo'; // Visible now!
}
return 'bar';

Install the Istanbul command-line tool nyc, we will use it in the package.json file for running the test coverage script.

$ npm i -D nyc

Test coverage is also often referred to as code coverage. However, there is a subtle difference. Test coverage: "how much test is written with respect to the functionaility of the code". Code coverage: "how much code is subjected to testing".

Add a script for test coverage in the package.json file and name it test-coverage (there is no restriction in the name).

package.json
{
  "name": "arithmetic",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "lint": "eslint .",
    "test": "mocha test",
    "test-coverage": "nyc mocha test"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "^4.2.0",
    "eslint": "^6.6.0",
    "mocha": "^6.2.2",
    "nyc": "^14.1.1"
  }
}

Now run the test-coverage script.

$ npm run test-coverage

> arithmetic@1.0.0 test-coverage /projects/arithmetic
> nyc mocha test

  arithmetic functions
    ✓ add() should add
    ✓ subtract() should subtract

  2 passing (8ms)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |       75 |       75 |      100 |       75 |                   |
 index.js |       75 |       75 |      100 |       75 |              6,14 |
----------|----------|----------|----------|----------|-------------------|

Only 75% test coverage! Looks like lines 6 and 14 are not covered.

Indeed, our current test suite only tests the functionality of the library with the expected parameters. Our smart library functions can detect missing parameters too, we should add tests for those feature as well.

If it were not for Istanbul, we wouldn't have known about those additional test cases; and that's why it's important to have test coverage.

Update the test suite to include additional tests.

test.js
const expect = require('chai').expect;
const lib = require('./index');
const add = lib.add;
const subtract = lib.subtract;

describe('arithmetic functions', () => {
  it('add() should add', () => {
    expect(add(2, 2)).to.equal(4);
  });

  it('add() should throw if any parameter is missing', () => {
    expect(add).to.throw();
  });

  it('subtract() should subtract', () => {
    expect(subtract(2, 2)).to.equal(0);
  });

  it('subtract() should throw if any parameter is missing', () => {
    expect(subtract).to.throw();
  });
});

Run the test-coverage script again.

$ npm run test-coverage

> arithmetic@1.0.0 test-coverage /projects/arithmetic
> nyc mocha test

  arithmetic functionss
    ✓ add() should add
    ✓ add() should throw if any parameter is missing
    ✓ subtract() should subtract
    ✓ subtract() should throw if any parameter is missing

  4 passing (10ms)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 index.js |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|

100% test coverage, the way it should be!

A 100% test coverage is not always mandatory. In some very complex projects 100% test coverage may not be feasible, and neither do 100% test coverage mean you have bug free perfect code. It is just one of the many metrics to guage the quality of a project. As long as you know what's going on, it is perfectly fine to not have 100% test coverage or have reduced test coverage.

Remote test coverage with Codecov#

Assuming you have 100% or an acceptable level of test coverage on your local machine, how do you share this fact with your team members?

Call everyone on a screen-sharing meeting and show them the "proof"? Ask everyone to check out your branch and "see for themselves"?

Doesn't sound efficient, does it?

We need to integrate a remote test coverage service like Codecov to show the state of test coverage in your pull request or branch on the repository hosting service (GitHub, BitBucket, GitLab etc.) itself.

Sign up on Codecov and got to https://codecov.io/gh/hacksparrow/arithmetic/settings and click on the "Create new webhook" button to connect your repository to Codecov. Make sure to give other required permissions to Codecov.

Then, create a codecov.yml file in your project directory:

codecov.yml
coverage:
  status:
    project:
      default: true

That's all you need to do.

Codecov has many more capabilities and options, but in this tutorial we will just focus on adding GitHub checks and having a Codecov badge on our README.md file.

Install codecov as development dependency:

$ npm i -D codecov

Then add script named codecov in the package.json file:

package.json
{
  "name": "arithmetic",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "lint": "eslint .",
    "test": "mocha test",
    "test-coverage": "nyc mocha test",
    "codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "^4.2.0",
    "codecov": "^3.6.1",
    "eslint": "^6.6.0",
    "mocha": "^6.2.2",
    "nyc": "^14.1.1"
  }
}

The codecov script will be responsible for running the test coverage script and uploading the result on Codecov.

Unlike test and test-coverage, this script is something we won't be running manually. It is more for the CI service than us. Infact, running npm run codecov will fail on your local machine, unless you have configured it with an Upload Token.

Private repositories need an Upload Token from Codecov, you can find it under "Repository Upload Token" in the repository's settings page - Eg: https://codecov.io/gh/hacksparrow/arithmetic/settings. Set it in Circle CI's "Environment Variables" page - Eg: https://circleci.com/gh/hacksparrow/arithmetic/edit#env-vars.

In the next section, we will find out how the codecov script integrates with the CI service.

CI with CircleCI#

And now, it's time to bring everything together under a CI service.

Sign up on CircleCI and add your project. In my case, the URL for adding projects is https://circleci.com/add-projects/gh/hacksparrow. Find your repository and click on "Set Up Project". The click on the "Start building" button to get started.

The build will fail on master since it is not configured for CircleCI yet. We will configure the repository for CircleCI in the next step.

Create a directory named .circleci in the project directory, and create a file in it named config.yml with the following content:

.circleci/config.yml
version: 2
jobs:
  lint:
    docker:
      - image: circleci/node:12.4
    working_directory: ~/repo
    steps:
      - checkout
      - run: npm install eslint
      - run: npm run lint
  node-6.0:
    docker:
      - image: circleci/node:6
    working_directory: ~/repo
    steps:
      - checkout
      - run: npm install
      - run: npm run test

  node-12.4:
    docker:
      - image: circleci/node:12.4
    working_directory: ~/repo
    steps:
      - checkout
      - run: npm install
      - run: npm run test

  codecov:
    docker:
      - image: circleci/node:12.4
    working_directory: ~/repo
    steps:
      - checkout
      - run: npm install
      - run: npm run test-coverage
      - run: npm run codecov

workflows:
  version: 2
  build:
    jobs:
      - lint
      - node-6.0
      - node-12.4
      - codecov
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

We have defined three jobs in the config file:

  1. lint - for linting
  2. node-6.0 - for ensuring our library supports Node.js version 6.0
  3. node-12.4 - for ensuring our library supports Node.js version 12.4
  4. codecov - for running the test coverage and uploading the results to Codecov.

These jobs may be named anything but using descriptive names is the best practice.

CircleCI, the best CI service in the world, has much more than what I have shown in this example. We will do more detailed CircleCI tutorials later; for now, let's just use something that's simple and easy to follow.

The first pull request#

Add *.lcov, .nyc_output, and coverage, to the .gitignore file:

.gitignore
node_modules
*.lcov
.nyc_output
coverage

Created a README.md file.

Get the Codecov badge from https://codecov.io/gh/hacksparrow/arithmetic/settings/badge (use your project's URL) and CircleCI badge from https://circleci.com/gh/hacksparrow/arithmetic/edit#badges (use your project's URL) and add them to README.md.

README.md
# Arithmetic

[![codecov](https://codecov.io/gh/hacksparrow/arithmetic/branch/master/graph/badge.svg)](https://codecov.io/gh/hacksparrow/arithmetic)
[![CircleCI](https://circleci.com/gh/hacksparrow/arithmetic.svg?style=svg)](https://circleci.com/gh/hacksparrow/arithmetic)

Commit the changes you have made so far and push it to the remote repository.

$ git add .
$ git commit -m 'Added dev tools'
$ git push origin dev-tools

Now go to the repository's page and open a pull request.

You will see CircleCI kicking into action immediately.

Then in a new moments, you notice that all GitHub checks has passed. The merge button is green.

Go ahead and merge the PR using the "Rebase and merge" option. Click on that down arrow for the option.

Then go to the homepage of the repository and proudly admire the 100% test coverage report from Codecov and "PASSED" status from CircleCI.

Summary#

Linting, testing, test coverage, and continuous integration (CI) are crucial aspects of all good quality software development projects. Some of them can be run locally while all of them can be brought together under a CI service. CircleCI, ESLint, Mocha, Istanbul, and Codecov make a great CI combination for Node.js projects.

Tweet this | Share on LinkedIn |