Continuous Integration with CircleCI, ESLint, Mocha, Istanbul, and Codecov for Node.js projects
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.
* 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:
{
"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:
exports.add = add;
exports.subtract = subtract;
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
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.
coverage
node_modules
Now create a .eslintrc.yml to configure eslint
for the project. Feel free to customize it.
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.
{
"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#
Install Mocha and Chai as development dependencies.
$ npm i -D mocha chai
Create a file named test.js and write the tests.
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.
{
"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).
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;
}
}
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
Add a script for test coverage in the package.json file and name it test-coverage
(there is no restriction in the name).
{
"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.
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!
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:
coverage:
status:
project:
default: true
That's all you need to do.
Install codecov
as development dependency:
$ npm i -D codecov
Then add script named codecov
in the package.json file:
{
"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:
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
We have defined three jobs in the config file:
lint
- for lintingnode-6.0
- for ensuring our library supports Node.js version 6.0node-12.4
- for ensuring our library supports Node.js version 12.4codecov
- 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.
The first pull request#
Add *.lcov
, .nyc_output
, and coverage
, to the .gitignore
file:
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.
# Arithmetic
[](https://codecov.io/gh/hacksparrow/arithmetic)
[](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.