How to correctly unit test Express server

Create and destroy an Express.js server in each unit test.

Let us take a simple ExpressJS server and see how to correctly unit test it. The server could be the very basic one given as an introductory "Hello World" example.

server.js
1
2
3
4
5
6
7
8
9
10
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.status(200).send('ok');
});
var server = app.listen(3000, function () {
var port = server.address().port;
console.log('Example app listening at port %s', port);
});
module.exports = server;

We can verify the server responds to a HTTP request from the command line

$ node server.js
Example app listening at port 3000
# from another terminal window execute
curl http://localhost:3000/
"ok"

Let us write a couple of unit tests to verify the server is working correctly. I will use mocha and supertest to execute http requests against the server.

spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var request = require('supertest');
describe('loading express', function () {
var server;
beforeEach(function () {
server = require('./server');
});
afterEach(function () {
server.close();
});
it('responds to /', function testSlash(done) {
request(server)
.get('/')
.expect(200, done);
});
it('404 everything else', function testPath(done) {
request(server)
.get('/foo/bar')
.expect(404, done);
});
});

Run the unit tests and they pass

$ mocha -R spec spec.js
  loading express
Example app listening at port 3000
    ✓ responds to /
    ✓ 404 everything else
  2 passing (109ms)

Notice a subtle problem - we create a new server before each unit test (in beforeEach callback). We assume this is working because we close the server in the afterEach callback. Thus we expect the following sequence of actions to happen

1
2
3
4
5
6
7
8
--- test 1
server starts
request to / is made and answered
server is closed
--- test 2
server starts
request to /foo/bar is made and answered with 404
server is closed

Yet, looking at the console we see only a single "Example app listening at port 3000" message. Something is not right!

First, why do we want to start and stop the server for each unit test? Aside from testing a complex scenario, we want to always test a clean server without any residue from the previous unit tests. Otherwise our tests will pass or fail depending on the order, which is an extremely undesirable and flaky testing approach. Starting and stopping the server for each unit test makes them order-independent.

Second, is the server closed after the first unit test finishes? Turns out, no. To properly close the expressjs server, we need to wait for all connections to close and only then let the Mocha test runtime know that it can continue. Even if we introduce a timeout, the server is NOT closed

does NOT close the server
1
2
3
4
afterEach(function (done) {
server.close();
setTimeout(done, 1000);
});

Instead we need to pass the Mocha's done callback to the server.close() call. Only then the server is going to be closed and the tests continue correctly.

properly closing the server after each unit test
1
2
3
afterEach(function (done) {
server.close(done);
});

We immediately hit a snag: the test framework reports that the server is not running when it tries to close the server after the second unit test.

Example app listening at port 3000
    ✓ responds to /
    ✓ 404 everything else
    1) "after each" hook
1) loading express "after each" hook:
     Error: Not running
      at Server.<anonymous> (net.js:1366:12)

I do not understand why the server would respond correctly to the second unit test - it seems to be the reference to the server + supertest side effect. What is interesting here is that the console message "Example app listening at port 3000" still appears only once before the first unit test and not before first and second unit tests.

Let us look again how we create the server before each unit test

1
2
3
beforeEach(function () {
server = require('./server');
});

Node module system caches the evaluated result of each module to avoid loading and compiling the same javascript file multiple times. Thus the server instance from server.js is only created once. The sequence of events is the following

1
2
3
4
5
6
7
8
9
--- test 1
server starts
request to / is made and answered
server is closed
--- test 2
cached instance of server (now closed) is returned from `require`
request to /foo/bar is made and answered with 404 (why?!)
afterEach tries to close the server, but it is already closed
exception is thrown

We have two ways to avoid caching the server.

Solution 1: export a factory function to create a new server instance on demand

server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
function makeServer() {
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.status(200).send('ok');
});
var server = app.listen(3000, function () {
var port = server.address().port;
console.log('Example app listening at port %s', port);
});
return server;
}
module.exports = makeServer;

We can create the server inside beforeEach callback

tests
1
2
3
beforeEach(function () {
server = require('./server')();
});

Now we get the correct messages and tests

$ mocha -R spec spec.js
Example app listening at port 3000
    ✓ responds to /
test 404
Example app listening at port 3000
    ✓ 404 everything else
  2 passing (124ms)

The server has been created and destroyed for each unit test. While this is correct, I would like to preserve the returning on the server instance instead of the factory function. Most applications will need just a single server instance, not multiple ones. Only the unit tests need a unique instance for each test. Thus we can revert back to the original server.js and use an alternative method to make sure we get a fresh server instance and not just the cached reference.

Solution 2: bust require cache to force the full 'server.js' reload

Node.js require call keeps an internal cache of loaded and evaluated code, stored as values in a plain object. The keys in the cache object are the resolved file paths to the source files. We can get the resolved path ourselves and delete the instance from the cache before calling the require

tests
1
2
3
4
beforeEach(function () {
delete require.cache[require.resolve('./server')];
server = require('./server');
});

While this is simple enough, I have a better utility for busting cache (and doing other things), that replaces Node's require with a more powerful version: really-need. In addition to a path, it accepts an options object; one of the available options is to bust the cache before loading a module.

1
2
3
4
require = require('really-need');
beforeEach(function () {
server = require('./server', { bustCache: true });
});

Everything is working correctly, and the low-level details are hidden from the user.

The complete unit testing code

spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var request = require('supertest');
require = require('really-need');
describe('loading express', function () {
var server;
beforeEach(function () {
server = require('./server', { bustCache: true });
});
afterEach(function (done) {
server.close(done);
});
it('responds to /', function testSlash(done) {
request(server)
.get('/')
.expect(200, done);
});
it('404 everything else', function testPath(done) {
console.log('test 404')
request(server)
.get('/foo/bar')
.expect(404, done);
});
});

the run output

1
2
3
4
5
6
7
$ mocha -R spec spec.js
loading express
Example app listening at port 3000
✓ responds to /
Example app listening at port 3000
✓ 404 everything else
2 passing (121ms)

Everything is running nicely.

Related: blog posts Testing Connect middleware and Server Running Inside Cypress Plugin Process.