Testing Express APIs with Supertest
Introduction
Hello, I'm Ilya, a full-stack developer who uses Node.js on the server and Ember.js on the client. Lately I've been having lots of fun testing my Express APIs, and I just wanted to show-off how fun and simple it can be. Hope you enjoy this as much as I enjoyed writing it!
Getting Started
For this tutorial we'll start with a simple Express API, and we will set up a test environment and do some integration tests on our API using supertest and tape. Supertest is a library made specifically for testing nodejs http servers, and tape is an assertion library that we'll use to setup our testing structure.
I've created a repository on Github, where you will find a fully working application with the API and the tests. This is the code that I will be referencing throughout this tutorial.
Let's begin! We'll start by installing all of our dependencies:
npm install --save express
npm install --save-dev supertest tape
Now we'll create our project structure, something like:
my-project/
|_ package.json
|_ index.js
|_ server/
| |_index.js
|_ test/
|_ index.js
This is my standard project structure when building Express apps. Now let's write that app!
Our API
Let us start with server/index.js
where we will write a super simple API, which will work with some user data.
'use strict';
var express = require('express');
var app = express();
var users = ['John', 'Betty', 'Hal'];
app.get('/api/users', function (req, res) {
res.json(users);
});
module.exports = app;
Note: Usually you'd make a call to a database, but for the sake of this tutorial, I'll use a simple fixture.
Now, if you notice, I'm not calling app.listen(..)
anywhere, that is because supertest takes an app
object, and doesn't need your app to be listening on a port. For our app to run we will add some code to our index.js
file, which will be the main file that is called when invoking npm start
.
'use strict';
var server = require('./server');
var port = process.env.PORT || 3000;
server.listen(port, function () {
console.log('Server running on port %d', port);
});
Now we require our app from above, and we listen on it there. This gives us the ability to require our server and pass it to supertest. So now if we ran npm start
we should see something like this:
Server running on port 3000
**
Note: ** If npm start
doesn't do anything, add a start script to your package.json, see here.
Now that's wonderful and exciting, but how do we know that our app works? Well, we could visit localhost:3000/api/users
and we'll see the resulting users. Maybe go ahead and try that now, I'll wait
Are We Satisfied?
I hope your API worked, but do we really want to do that every time we make a change? No, because we have problems to solve and apps to build that will solve those problems.
This is where supertest comes into play, but before we can do that, let's setup our first dummy test, we'll do this in test/index.js
.
'use strict';
var test = require('tape');
test('First test!', function (t) {
t.end();
});
Now this is the most simple of tests, because it doesn't do anything! Here we are using tape to create a test, and we are letting it know that the test has finished. The API for tape is very simple, and should be relatively easy to grasp.
We can run this test if we modify our test script in package.json
to node test
, which will run test/index.js
with node. So now if we run npm test
, we'll see something like:
TAP version 13
# First test!
1..0
# tests 0
# pass 0
# ok
This looks about right, and our test passed, but it's kind of ugly and unreadable. We can spice it up with npm install --save-dev tap-spec
and by modifying our test script to node test | tap-spec
.
Test It
Now that we can run a test, let's setup out API test. We do this by adding supertest and importing our app.
var request = require('supertest');
var app = require('../server');
That is all the setup that we need before we write our test.
test('Correct users returned', function (t) {
request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200)
.end(function (err, res) {
t.end();
});
});
If we run this test as it is, we should have a passing result (although you'll see that it's 0 out of 0 tests, since we didn't assert anything with tape), and that is good because just from this code we know that our request was successful due to the 200
status code, and we are getting json back, just as expected since we used res.json
.
Let's see if our result is as expected. For this we'll assert if our users are the correct users. We'll do this from inside the callback that we passed to end
, which will return as an error or our response.
'use strict';
var test = require('tape');
var request = require('supertest');
var app = require('../server');
test('Correct users returned', function (t) {
request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200)
.end(function (err, res) {
var expectedUsers = ['John', 'Betty', 'Hal'];
t.error(err, 'No error');
t.same(res.body, expectedUsers, 'Users as expected');
t.end();
});
});
Now we're getting somewhere!
We now have two assertions of the result that we have. First of all we test that there isn't an error by using t.error()
. We follow by confirming that our users are indeed correct; for this we use t.same()
, and we compare the result, which is stored in res.body
, as expected.
We should now see the following when we run npm test
:
Correct users returned
✓ No error
✓ Users as expected
total: 2
passing: 2
duration: 135ms
All tests pass!
Congratulations, you wrote your first API test!
From here you can get creative, and test express routers and middleware (with a little bootstrapping), and remember to visit the superagent documentation, because you can use those methods with supertest (supertest is based on superagent).
The full code is located in the testing-express-api repository.
I know this is old but I am barely now learning about Tape since I’ve stumbled upon some integration tests written using Tape…am I reading this right? Each assertion is considered to be a test in Tape? So it’s not the test block that is a test but the assertion is the test as reported in the run results output?
I think asserts are not counted as ‘tests’, since it does print out the test as well with the assertions nested under it.
“doesn’t need your app to be listening on a port”
It turns out that supertest actually won’t work if your app.js file is also listening to a port. So ‘not listening’ is not optional. Once I moved my app.listen to a separate file, supertest worked fine.
Thanks for this nice intro!
Yeah, generally I split my apps into two files, index.js at the root level which imports src/app.js and then calls listen on it. The test would import src/app.js directly instead.
Can you explain how to use database-backed fixtures for testing super api? Is there an easy way to do that? What’s in my mind is to use server’s api to create testing data on a testing db. As I was using this approach in Java since 2005s…
That is definitely the easiest way, another possibility is using an in-memory db alternative if using some kind of orm layer on top. This does break down if you use specialty features of the db that aren’t available across all db alternatives.