Using supertest to avoid manually testing your endpoints

The scenario: In order to test your endpoints/routes in Express/Koa/whatever you're using, you might currently be using a tool like Postman to send HTTP requests and make sure you're getting back the expected responses / the right code is being executed.

Or maybe you're testing the routes from the front-end of your application.

Either way, the problems with these methods are: - they're manual, as opposed to automated - they make it difficult to test error scenarios.

Not to mention that if you have tons of endpoints in your REST API, you could end up with the same problem you have with you browser tabs - you have so many of them open it's hard to find any single one, like below.

To be clear, I'm not denigrating Postman or frontend testing whatsoever - "functional testing" (as this type of testing is usually referred to) is immensely helpful and has its place. It's really useful when you want to make ad-hoc requests just to test some stuff out, or when you want to show another team how the API works and what the expected request/response structure is (Swagger/OpenAPI is really useful for this).

But that's more in the documentation realm, I'd say. Maybe even test-ish

But it's not part of a strong development testing process. I.e. - unit tests, integration tests, end-to-end tests.

It can't be automated, it can't run as part of your CI/CD pipeline, and it can't catch regression bugs before they go into production.

Enter supertest

Fortunately, there is a much more robust way of adding automated tests for your endpoints in Node. And that is supertest.

Supertest essentially lets you write those automated tests for your routes/endpoints.

Let's go over some common HTTP things you might want to write tests for... things you might be doing manually now that you can automate.

NOTE: we import supertest as request in the tests below

GET routes

In order to test GET routes we use .get():

it('should return a 200 with successful items', async () => {
  await request(app)
    .get('/api/item')
    .set('Accept', 'application/json')
    .expect('Content-Type', /json/)
    .expect(200)
    .then(res => {
      expect(res.body).to.deep.equal({baseball: 23, baseball_glove: 13, basketball: 53})
    })
})

We can assert on many things. Here we're using supertest's built-in assertion method - .expect() - to check that the response header and HTTP status code are correct. We're also using Chai's expect to make sure the data returned is correct too.

We can also make requests using query strings, here's what that looks like:

it('should accept a query string', async () => {
  await request(app)
    .get('/api/item')
    .query({term: 'soccer cleats'})
    .expect(200)
    .then(res => {
      expect(res.text).to.equal('soccer cleats')
    })
})

The key thing to notice here is we use .query() and pass it the query string in object form - the "term" here in this case would be the term part of the query, and the value is obviously the string value, like so https://yoururl.com/api/item?term=soccer%20cleats

POST routes

We can also test POST routes by using .post() and .send() to send the POST body:

it('should return a 201 when an item is successfully created', async () => {
  await request(app)
    .post('/api/item')
    .send({item: 'fishing rod'})
    .expect(201)
})

PUT routes

Testing PUT routes is pretty much the same as POST routes - we still use .send() to send the request body, but instead of .post() it's .put()

DELETE routes

The API for testing DELETE routes via supertest is .delete().

Headers

We can also set headers on the request and expect headers on the response. Here's the GET example showed earlier:

it('should return a 200 with successful items', async () => {
  await request(app)
    .get('/api/item')
    .set('Accept', 'application/json')
    .expect('Content-Type', /json/)
    .expect(200)
    .then(res => {
      expect(res.body).to.deep.equal({baseball: 23, baseball_glove: 13, basketball: 53})
    })
})

Notice that it uses .set() to set the request header and then the usual .expect() to test that we got the correct response header.

Cookies

No testing tool would be complete without being able to test for cookies!

Here's the app code:

app.get('/cookie', (req, res) => {
  res.cookie('cookie', 'example-cookie')
  res.send()
})

And here's the test code:

it('should save cookies', async () => {
  await request(app)
    .get('/cookie')
    .expect('set-cookie', 'cookie=example-cookie; Path=/')
})

Notice that we check the headers on the response to check for the cookie.

Authentication

If you have a route that expects authentication - like a user login for example - use .auth() to pass the authentication credentials, like below:

it('should work with an authenticated user', async () => {
  await request(app)
    .post('/api/user')
    .auth('username', 'password')
    .expect(200)
})

Other HTTP status codes

Lastly, it might be obvious at this point but worth making abundantly clear, you can write tests for whatever HTTP status codes you want. Here are two examples for "500" and "404" errors:

it('should return a 500 in case of an error', async () => {
  await request(app)
    .post('/api/item')
    .send({bad_data: 'afafaf'})
    .expect(500)
})
it('should 404 for nonexistent route', async () => {
  await request(app)
    .get('/store')
    .expect(404)
})

Wrapping up

Next time you find yourself relying on manual testing to make sure things with your REST API haven't broken, instead of doing it manually use supertest to automate it.

Your life will be so much better because your software will be much more robust and you'll catch regression bugs much quicker.

Want a repo with the full code above so you can immediately start playing around with supertest? Subscribe below! You'll also get any updates to the code as well as new semi-weekly posts delivered directly to your inbox as soon as I hit "publish".

Subscribe for the code!

No spam ever. Unsubscribe any time.