JS Unit Testing Made Easy

Getting a high coverage rate in Javascript apps can be easier than you think. In the last two years, Jest has become so much faster and useful for testing everything JS related. Before then, you had to pull in other libaries (such as sinon) to get spies, test coverage reporting, and more.

I'm going to go over the process of building a single test case that initially looks tough to test, but we'll simplify our code so that our tests can run anywhere, all that's needed is Node. We won't need to spin up a database or have any other services running in order for our tests to run. This isn't only convient, but when we fake 3rd party services, our tests will run faster too. We'll be doing this all without any jest.mock calls too, because our code will be as pure and functional as possible.

Let's go

First, let's make a a basic node server using express. Don't get turned off because this is "only" an express app and not a nifty graphql server. I chose to use express because all of the principles shown will relate directly to anything you build across any application or language.

server.js

const express = require('express');
const Appointment = require('./models/appointment')

const app = express();


app.get('/appointments', async (req, res) => {
  let appointments = await Appointment.all();

  if(appointments.length === 0) return res.send('No appointments')
  res.send(appointments);
}

Sure, we wouldn't return plain text from our api if there were no appointments, but keeping it simple and having a conditional in our endpoint will help us make a more realistic test while keeping it easy to understand.

How would we test this?

The naive approach would be to go install supertest which will hit your express endpoint and invoke everything. The problem with this is twofold. We can't make a unit test the way our code is currently written, and we'd be writing integration tests that hit express itself. It's a waste because we shouldn't test third party library, only our individual lines of code, since we are wanting to create high-quality unit tests, why waste time hitting express too?

We can refactor this code by making a new appointments.js file and extracting our express route function there:

appointments.js

const Appointment = require('./models/appointment')

module.exports = async (req, res) => {
  let appointments = await Appointment.all();

  if(appointments.length === 0) return res.send('No appointments')
  res.send(appointments);
}

Now, server.js looks like this:

const express = require('express');
const appointments = require('./appointments')

const app = express();
app.get('/appointments', appointments)

If you can't tell already, now we can test our route by itself and pass in fake request and response variables! Let's install Jest now, don't worry, it's not a hassle:

npm install jest babel-jest babel-core --save-dev
npx jest --watchAll

Jest will automatically use babel when needed for language features not support in your node version.

Let's create appointments.test.js now. Jest will instantly find the file and be waiting for us to write our first test:

const appointments = require('./appointments')

test('Returns all appointments', () => {
  let send = jest.fn()
  
  appointments(null, { send })
  
  expect(send.mock.calls[0][0]).toEqual('No appointments')

})

Read up on jest.fn if you're not familiar.

If our fake database module returned an empty array, this test would be passing. We just tested our appointments endpoint without touching express itself. I hope you see the power of splitting out your functions now. But I did say we would avoid hitting the database all together. This is the point where I thank the TC39 for adding function currying, because we can use it to pass in our database layer and avoid mocking modules with a much cleaner syntax than in the past. Let's change our server.js:

const express = require('express');
const appointments = require('./appointments')
const Appointment = require('./models/appointment')

const app = express();
app.get('/appointments', appointments(Appointment))

In our server, we are passing the database module to the appointments function. Let's do some currying in our appointments function:

module.exports = Appointment => async (req, res) => {
  let appointments = await Appointment.all();

  if(appointments.length === 0) return res.send('No appointments')
  res.send(appointments);
}

Now, our database layer is a function parameter. Testing just got even easier! Let's update our appointments.test.js now:

const appointments = require('./appointments')

test('Returns all appointments', () => {
  let send = jest.fn()
  let fakeAppointments = { 
    all: async () => [],
  }
  
  appointments(fakeAppointments)(null, { send })
  
  expect(send.mock.calls[0][0]).toEqual('No appointments')

})

We made an object that you can call .all() on and it will return an empty array. This test now passes, and it doesn't hit our database layer at all! It hit's the logic that matters. If we change our appointments.js to do nothing when no appointments are found on accident:

module.exports = Appointment => async (req, res) => {
  let appointments = await Appointment.all();
  
  res.send(appointments);
}

The test will fail, just like we would expect. In our case we can run jest without hitting our server.js file, which is all function calls to express which we expect to have already been tested by the libary owner.

Let me know if explaining my thought process when it comes to testing helped. I think of all these pieces while writing tests for frontend applications as well. If you need clarification on something please reach out below or on Twitter @zachcodes.