Testing an Express Server

A couple weeks ago, I wrote a suite of tests for a new Node-Express service.

Up until that point, all the JS testing I’d done was for React components. Writing tests for an Express app is a little different, but was actually really fun and easy with the right tools. So here’s a little write-up on some testing tools and tips for backend Javascript!

Tools

You will need one or more of the following tools:

  • Mocha: a test runner. In package.json, simply add "test": "mocha" under “scripts” and all tests in the test directory will run.
  • Chai: an assertions library for Javascript, offering both an expect API and an assert API.
  • Supertest: an HTTP testing library that lets you easily send HTTP requests to a local server.
  • Node-tap: a test library implementing the Test-Anything-Protocol for Node.

Most Valuable Player: Supertest

My favorite new tool on this list is the Supertest library. It is SO easy to hit each Express endpoint with a Supertest request and examine the response.

The way it works: require Supertest and pass it an Express application. This returns a supertest object onto which other methods can be chained. For example, you can set attributes with “get(‘/path’)” or “set(‘Content-Type’, ‘application/json’)” that modify both the object and your eventual request to the server. Expectations for the response are also chained onto the supertest object.

With mocha/chai/supertest:

1
2
3
4
5
6
7
8
9
const request = require(supertest)
const app = require('../app')
it('gets a 200 response', function (done) {
        request(app) // this returns a SuperTest object
        .get(/data)
        .set('Content-Type', 'application/json')
        .expect(200)
        .end(done)
})

Asynchronicity

One interesting aspect of writing tests for an Express server is the fact that your tests must run asynchronously. After all, you’re sending a request to your server; if your tests proceeded onwards full steam ahead, you might run into an expectation that checks for a response before the response has even arrived. Mocha also needs wait for the current test to finish before heading off to other tests.

Mocha has two ways of handling asynchronous tests:

  • pass in a done callback that is called when the test ends, or
  • use Mocha’s built-in Promise handling.

Now that Mocha actually handles Promises in-house, it’s considered better practice to use .then and .catch rather than .end(done).

With mocha/supertest:

1
2
3
4
5
6
7
8
9
10
const request = require(supertest)
const app = require('../app')
it('gets a 200 response', function () {
        request(app)
        .get(/data)
        .then((res) => {
           if (!(res.status === 200)) { throw new Error } // make sure to actually throw an error so the test fails
        })
        .catch(err => {throw err})
})

Node-tap can also be used this way, with .then and .catch callbacks on a Promise.

node-tap/supertest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const request = require(supertest)
const test = require('tap').test
const app = require('../app')
test(gets a 200 response', (t) => {
  t.plan(1)
  request(app)
    .get(‘/data')
    .then((res) => {
      t.equal(res.status, 200)
    })
    .catch((err) => {
      t.fail(err)
    })
})

Now sit back, relax, and watch your tests run.