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.
1 | var express = require('express'); |
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.
1 | var request = require('supertest'); |
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 | --- test 1 |
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
1 | afterEach(function (done) { |
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.
1 | afterEach(function (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 | beforeEach(function () { |
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 | --- test 1 |
We have two ways to avoid caching the server.
Solution 1: export a factory function to create a new server instance on demand
1 | function makeServer() { |
We can create the server inside beforeEach
callback
1 | beforeEach(function () { |
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
1 | beforeEach(function () { |
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 | require = require('really-need'); |
Everything is working correctly, and the low-level details are hidden from the user.
The complete unit testing code
1 | var request = require('supertest'); |
the run output
1 | $ mocha -R spec spec.js |
Everything is running nicely.
Related: blog posts Testing Connect middleware and Server Running Inside Cypress Plugin Process.