A few weeks back I was doing some refactoring of the api code for the OpenStore. Not wanting to do a bunch of manual testing, I decided it was time to implement some testing to better ensure that restrictions around app management are working as expected. For this I decided to use the tools Mocha, Chai, Sinon, SuperTest, and factory-girl. Why these tools in particular? Mostly due to familiarity from a prvious job. This approach is working for me, so I wanted to write a tutorial to help others get setup with a testing environment for their Node.js api.

The App

The OpenStore is an Express api using the Mongoose ORM. So for the purpose of this tutorial I’ll use an example app with the same technologies. But this testing setup should easily be usable with other frameworks and ORMs thanks to the generic nature of the tools involved. For this tutorial I’ll walk through setting up a new, simple api. Then we’ll add testing and linted. If you have an existing api you can skip over the setup and jump straight to implementing the testing.

Setup and install dependencies

To get started you’ll need to setup a new Node.js project with npm init. Then install the necessary dependencies: npm install --save mongoose express body-parser. Once the dependencies are installed, add an npm script start to call node ./index.js. This will simplify running the code. Then you can add the following two files to the project:

index.js

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const express = require('express');
const bodyParser = require('body-parser');
const Widget = require('./widget');

const app = express();
app.use(bodyParser.json());


app.get('/', async (req, res) => {
    try {
        let widgets = await Widget.find();
        res.send(widgets);
    }
    catch (err) {
        res.status(500);
        res.send();
    }
});

app.get('/:id', async (req, res) => {
    try {
        let widget = await Widget.findOne({_id: req.params.id});
        res.send(widget);
    }
    catch (err) {
        res.status(500);
        res.send();
    }
});

app.post('/', async (req, res) => {
    let widget = new Widget({
        foo: req.body.foo,
        bar: req.body.bar,
    });

    try {
        await widget.save();
        res.send(widget);
    }
    catch (err) {
        res.status(500);
        res.send();
    }
});

app.put('/:id', async (req, res) => {
    try {
        let widget = await Widget.findOne({_id: req.params.id});
        widget.foo = req.body.foo;
        widget.bar = req.body.bar;

        await widget.save();
        res.send(widget);
    }
    catch (err) {
        res.status(500);
        res.send();
    }
});

app.delete('/:id', async (req, res) => {
    try {
        let widget = await Widget.findOne({_id: req.params.id});
        await widget.remove();

        res.status(204);
        res.send();
    }
    catch (err) {
        res.status(500);
    }
});

app.server = app.listen(8080);
module.exports = app;

This sets up a very basic CRUD Express app that listens on port 8080. The last two lines expose the server so the api can be easily tested.

widget.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const mongoose = require('mongoose');

const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/test';
mongoose.connect(mongoUri, {useNewUrlParser: true}, (err) => {
    if (err) {
        console.error('database error:', err);
        process.exit(1);
    }
});

const WidgetSchema = mongoose.Schema({
    foo: String,
    bar: String,
});

module.exports = mongoose.model('Widget', WidgetSchema);

This sets up a simple Mongoose schema that gets used in index.js. It will connect to a local MongoDB server, so make sure you have one running locally. Or you can set the MONGODB_URI environment variable to use an Mongo server you have access to.

Now you can run npm start and interact with the api via curl or a tool like Postman. Some curl examples:

# Get all
curl http://localhost:8080

# Get by id
curl http://localhost:8080/5caca4e450fc8a552e9c37f6

# Create widget
curl -X POST -H "Content-Type: application/json" --data '{"foo": "test", "bar": "testing"}' http://localhost:8080

# Update widget
curl -X PUT -H "Content-Type: application/json" --data '{"foo": "test2", "bar": "testing2"}' http://localhost:8080/5caca5be1e9a365729a083bb

# Delete widget
curl -X DELETE http://localhost:8080/5caca5be1e9a365729a083bb

Testing the App

Now that we have an api up and running we can start writing tests for it. To do that you will need to install several dependencies: npm install --save-dev mocha chai factory-girl sinon sinon-chai supertest. Each of these packages has a different role to play in testing our app. Mocha is a testing framework for JavaScript. Chai is an assertion library that complements Mocha. factory-girl is a factory library that makes it simple to generate test data models. Sinon is a library for mocking, stubbing, and spying function calls for your tests. sinon-chai is an extension to Chai that allows you to assert calls to your mocks from Sinon. SuperTest is a library for testing http servers.

Once everything finishes installing, add a new script to your package.json called test and have it run the mocha command. Then you can create the following files:

test/setup.js

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
const chai = require('chai');
const FactoryGirl = require('factory-girl');
const mongoose = require('mongoose');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');

const app = require('../index');

require('./factories/widget');

FactoryGirl.factory.setAdapter(new FactoryGirl.MongooseAdapter());

chai.use(sinonChai);

before(function(done) {
    // Wait for the mongoose to connect
    if (mongoose.connection.readyState != 1) {
        mongoose.connection.once('open', done);
    }

    this.sinon = sinon;
});

afterEach(async function() {
    await FactoryGirl.factory.cleanUp();
    this.sinon.restore();
});

after(() => {
    app.server.close();
    mongoose.connection.close();
});

This setup module will integrate the various different testing tools and get everything setup so we can start testing our api. The before function will run once before any tests get run. This before function ensures that Mongoose is connected to the local database (make sure you have Mongo running on your machine). The afterEach function runs after every test has completed. It calls the cleanup/restore methods so that FactoryGirl and Sinon are clear for the next test. Lastly, the after function will run once after all the tests are complete and it will shutdown the server and Mongo connection so the test process doesn’t hang when it’s done.

test/factories/widget.js

1
2
3
4
5
6
7
8
const {factory} = require('factory-girl');

const Widget = require('../../widget');

factory.define('widget', Widget, {
    foo: factory.sequence('Widget.foo', (n) => `foo${n}`),
    bar: factory.sequence('Widget.bar', (n) => `bar${n}`),
});

The widget factory simply sets up the rules for generating new widgets. These will be persisted in the database and be made available in our tests. For more information on how to setup a factory, check out this FactoryGirl tutorial.

test/api-test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const {expect} = require('chai');
const {factory} = require('factory-girl');
const request = require('supertest');

require('./setup');
const app = require('../index');

describe('API Test', () => {
    context('GET all', () => {
        it('is successful with multiple widgets', async () => {
            await factory.createMany('widget', 5);

            let res = await request(app).get('/').expect(200);

            expect(res.body).to.be.lengthOf(5);
        });

        it('is successful with no widgets', async () => {
            let res = await request(app).get('/').expect(200);

            expect(res.body).to.be.lengthOf(0);
        });
    });
});

Now that the setup is all out of the way, the real fun begins. This module creates two tests (defined by the calls to it). describe and context are ways for you to group related tests. Each group of tests can have their own before/beforEach/afterEach/after functions. The first test creates multiple widgets via the factory that we setup earlier. It then uses SuperTest to make a GET call to the / route that was setup in the Express app. Naturally, we expect it to return a 200 response and contain all of the widgets that we setup via the factory. This example is using the expect style behavior-driven development (BDD) methods of Chai. There are several different options provided by Chai and you can check out a comparison of them in the Chai docs. The second test is similar to the first, except it doesn’t create any widgets via the factory so the expected result is empty.

These first two tests covered some successful paths of the GET / route. What about the error handling part of that route? This is where Sinon comes in. Sinon can do many different things like spy on a method to see if it gets called. But for this next test we will stub the Widget.find method to make it return a rejected promise so the error handling can be tested. Add the following to the GET all context for the api-test.js module (and also import the widget class: const Widget = require('../widget');):

1
2
3
4
5
6
7
it('fails with a 500 error', async function() {
    let findStub = this.sinon.stub(Widget, 'find').rejects();

    await request(app).get('/').expect(500);

    expect(findStub).to.have.been.calledOnce;
});

Sinon replaces the Widget.find method with a stub function that returns a rejected promise. So rather than making a call to the database this will cause the request to return a 500 error. At the end of the test we’ll check that the stub function got called and that the test didn’t fail for a different reason.

Code Coverage

Since we have a few tests setup, let’s start tracking some metrics around how much of our code is getting tested. For this, install Istanbul’s nyc command: npm install --save-dev nyc. In order to start using nyc, you can put it before mocha in your test script. Alternatively you can make a new script called coverage to call nyc npm run test. Now if you run the updated script, you will first see the test output. Once that is completed, you’ll get a table of source files, code coverage metrics, and uncovered lines. This can be a good indicator of where to focus your testing efforts.

Linting the App

In addition to testing it is also good to lint your code to keep it consistent, especially when working with other developers. ESLint is a great tool for linting JavaScript code. Install it by running npm install --save-dev eslint. To use ESLint it’s best to pair it with a configuration. There are two popular configs: Airbnb and Standard. I have my own config based on the Airbnb one, for this tutorial I’ll be using it. Feel free to install whichever you choose. Install it and the plugin import modules: npm install --save-dev eslint-config-bhdouglass eslint-plugin-import. Now you can create a file .eslintrc.js, this will tell eslint how to behave.

.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
    extends: [
        'eslint-config-bhdouglass',
    ],
    env: {
        node: true,
        mocha: true,
    },
    rules: {},
    overrides: [
        {
            files: "*-test.js",
            rules: {
                'no-unused-expressions': 'off',
            },
        },
    ],
};

This config file tells ESLint which config we want to follow, in this case eslint-config-bhdouglass. Then we’ll need to let ESLint know that we are running a Node environment and we are also using Mocha. This will prevent errors on our tests for Mocha’s globals. If you want to customize the linting rules for this project you can set them in the rules section. You can see the list of available rules in the ESLint docs. Lastly we need to set specific rules on our test files to prevent ESLint from giving us issues with lines like expect(findStub).to.have.been.calledOnce;. Since the configuration is completed, you can now add a new script to your package.json, called lint and make it call eslint. Running npm run lint will invoke ESLint with the configured rules and it will spit out some warning and errors as it files them.

CI Setup

After writing all those tests and hooking up linting the final thing to do for this tutorial is to make these processes run automatically in CI. To do this we’ll use the GitLab CI because its very simple to use. Create the following file in the root of your project:

.gitlab-ci.yml

1
2
3
4
5
6
7
8
9
services:
  - mongo:latest

test:
  image: node:latest
  script:
    - npm install
    - npm run lint
    - MONGODB_URI=mongodb://mongo:27017/test npm run test

Since the tests hit the Mongo database, the first few lines are needed to setup a Docker container running mongo. If you need a specific Mongo version you can change it to one of the available versions. After the service we setup the test “stage”, this will use the latest Node docker image (as with Mongo, you can use any available version). Now for the actual testing/linting. As part of the script, first all the necessary dependencies need to be installed, then it will run the lint command that was setup previously. After linting it will run the test command with an environment variable to use the Mongo service that was configured earlier. If both linting and tests run successfully the test stage will be successful. The only thing left to do is commit the .gitlab-ci.yml file and push the code to GitLab.

Closing

If you want to see the complete example api with tests, linting, and CI just check out the project on GitLab. If you have an questions or suggestions feel free to reach out to me via my website or file an issue on the GitLab project.

Resources