Author: David Fekke
Published: 9/1/2021
Whether you are doing test driven development (TDD) or are just looking for a way to add automated testing to your express app, this can be accomplished fairly easily using many different unit testing frameworks. With Node.js, I have used a number of different testing frameworks. One of the nice things about Node is that there are no shortage of options when it comes to testing.
The testing framework I prefer to use is Jest, but it is not a requirement for unit testing an express application. Jest is extremely popular with React developers, but it can be used with just about any JavaScript application.
I like to use Jest because on top of having all of the tools needed to run unit tests, it also can check code coverage.
Writing Testable Code
One of the good things about TDD is that helps developers write code that is more loosely coupled, which not only makes the code more easy to test, but makes the code more reusable.
Express follows a basic structure for responding to requests based on a route signature, i.e.;
app.get('/users/report', function(req, res) {
res.render('userreport', { title: 'Users Report' });
});
It is a good practice to break the handler code into its own function, and then use that handler in the route.
function userReportHandler(req, res) {
res.render('userreport', { title: 'Users Report' });
}
app.get('/users/report', userReportHandler);
Not only does this make the express code more organized, now we test just the handlers without having to run express.
Adding Jest
To add Jest to your application, we can do this by running the following command in the root of our application directory;
> npm i jest --save-dev
This will install Jest tooling into our node_modules folder. Now that Jest is installed, lets’ change the scripts
section of our package.json
file to run jest in the test
property. It should look like the following in our scripts
section;
"scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
"start": "node <name_of_mainjs_file>"
},
Lets’ take a quick look at what this command is doing. We are telling Node
to run the jest command in the node_modules/.bin/jest
location. This in turn runs the jest-cli. We are also running a flag --experimental-vm-modules
to allow us to run ESModule import/export syntax. We are also using jest’s --coverage
flag to get a code coverage report to let us know what percentage of our code is covered by unit tests.
Express App Example
For the purposes of this example, I am going to create a simple Express app that has two routes. One that will operate as a home page with a route of /
, and one that has a route called /hello
that takes one input parameter called :name
.
// routes/default.js
function index(req, res) {
res.send('hello world!');
}
function hello(req, res) {
const name = req.params.name ?? "world";
res.send(`hello ${name}!`);
}
export { index, hello };
// routes/main.js
import { Router } from 'express';
import { index, hello } from './default.js';
const router = Router();
router.get('/', index);
router.get('/hello/:name', hello);
export default router;
// app.js
import express from 'express';
import http from 'http';
import router from './routes/main.js';
const app = new express();
app.use('/', router);
const port = process.env.PORT || '3000';
app.set('port', port);
const server = http.createServer(app);
server.listen(port);
Now we can add a folder called __tests__
to our project. Jest automatically looks for any JavaScript or TypeScript files inside of this directory.
Before we create our first test, lets add supertest
to our project using the following command;
npm i supertest --save-dev
Supertest is used to mock out our express server so we do not have to run a http server in order to test our routes.
Unit Tests
Lets’ create a unit test just to test our route handlers.
import { index, hello } from '../routes/default.js';
describe('Test Handlers', function () {
test('responds to /', () => {
const req = { };
const res = { text: '',
send: function(input) { this.text = input }
};
index(req, res);
expect(res.text).toEqual('hello world!');
});
test('responds to /hello/:name', () => {
const req = { params: { name: 'Bob' } };
const res = { text: '',
send: function(input) { this.text = input }
};
hello(req, res);
expect(res.text).toEqual('hello Bob!');
});
});
Both of our route handlers both use the send function on the res
object, so I created a simple mock response object that duplicates what the Express response object would do if we used this in a real application. In this case send
just echos out whatever we pass in onto the text
property.
In both of my unit tests I am using jest’s expect
function to compare our expected results in the toEqual
function. If they match, both tests will pass.
Now lets’ add another test suite with supertest
to test the routes. We will create a new file called routes.t.js
to test the actual routes.
import request from 'supertest';
import express from 'express';
import router from '../routes/main.js';
const app = new express();
app.use('/', router);
describe('Good Home Routes', function () {
test('responds to /', async () => {
const res = await request(app).get('/');
expect(res.header['content-type']).toBe('text/html; charset=utf-8');
expect(res.statusCode).toBe(200);
expect(res.text).toEqual('hello world!');
});
test('responds to /hello/:name', async () => {
const res = await request(app).get('/hello/jaxnode');
expect(res.header['content-type']).toBe('text/html; charset=utf-8');
expect(res.statusCode).toBe(200);
expect(res.text).toEqual('hello jaxnode!');
});
test('responds to /hello/Annie', async () => {
const res = await request(app).get('/hello/Annie');
expect(res.header['content-type']).toBe('text/html; charset=utf-8');
expect(res.statusCode).toBe(200);
expect(res.text).toEqual('hello Annie!');
});
});
Using supertest
we can see these tests our more thorough and accurate of what our express app is doing. In the test suite above we use supertest to run the specific routes. We can also look at specific express properties to make sure the application is returning the expected results.
In all three unit tests we use expect
to check that we have the correct header results, statusCode and text.
Running our tests
To test our unit tests, all we have to do is run npm test
in out command line.
> npm test
> expresstest@1.0.0 test
> node --experimental-vm-modules node_modules/.bin/jest --coverage
(node:56591) ExperimentalWarning: VM Modules is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
PASS __tests__/routes.t.js
PASS __tests__/handlers.t.js
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
default.js | 100 | 50 | 100 | 100 | 7
main.js | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 0.782 s, estimated 1 s
Conclusion
As you can see from the example above it is actually quite easy to add unit testing to your express app.
I had a former co-worker who had the following slogan in his cubicle; “Test, test, test, and once you think you are done, test again”. Unit testing is just one small aspect to the quality assurance of your code, but if used correctly with CI/CD it will help you discover bugs well before they even make it to a staging or test environment.