Supertest: How to Test APIs Like a Pro

Modern software wouldn't be possible without HTTP APIs. In a microservice architecture, they connect front-end and back-end components or different…

Testim
By Testim,

Modern software wouldn’t be possible without HTTP APIs. In a microservice architecture, they connect front-end and back-end components or different back-end components. Developers also rely on third-party APIs for essential product features, such as payment and communications. Every API is a contract that both sides must follow to the letter to stay connected. In the case of APIs, that means that status codes, headers, and response bodies must always match the API documentation. For an API provider, functional testing is crucial in ensuring they can constantly deliver what they promise. Even API consumers may choose to run tests for third-party APIs. Whether you want to test internal or external APIs, you need good tooling. This article will introduce SuperTest, a Node.js testing library for HTTP APIs. We’ll discuss the library in general and then give you step-by-step instructions for creating your first API test.

What Is SuperTest?

SuperTest is a Node.js library that helps developers test APIs. It extends another library called superagent, a JavaScript HTTP client for Node.js and the browser. Developers can use SuperTest as a standalone library or with JavaScript testing frameworks like Mocha or Jest.

Expand Your Test Coverage

Fast and flexible authoring of AI-powered end-to-end tests — built for scale.
Start Testing Free

Getting Started With SuperTest

SuperTest is available from NPM. You can install it by typing the following command in your console:

npm install supertest --save-dev

In your JavaScript code, you can get a request object from SuperTest with this code:

const request = require('supertest');

When making a request, you can initialize it in two different ways. You can test an external API or an internal API already deployed and running (at least in a staging environment) by providing the URL as a parameter. Here’s an example of making an external API request. We use the public Dog API that offers open-source images of dogs. It’s a simple API that requires no registration and, as such, is perfect for demonstrations of using external APIs:

const request = require('supertest');

request('https://dog.ceo')
  .get('/api/breeds/image/random')
  .end(function(err, res) {
        if (err) throw err;
        console.log(res.body);
  });

The get() method tells SuperTest that we’re using the HTTP GET verb. You can chain additional methods into the call for setting authentication, custom HTTP headers or body, etc., but this is not necessary for this example. The end() method finalizes the request and calls the API server. As its parameter, it requires a callback function for handling the response.

What about the other way for initializing your request? You can also pass the app object if you built your API with a Node.js framework like Express. In the latter case, SuperTest will start a test server if necessary and automatically send requests there. Here’s an example of creating an Express app with one endpoint (inspired by the Dog API) and then requesting that same endpoint:

const request = require('supertest');
const app = require('express')();

app.get('/api/breeds/image/random', function(req, res) {
    res.status(200).json({ 
        message : 'https://example.com/',
        status : 'success'
    });
});

request(app)
  .get('/api/breeds/image/random')
  .end(function(err, res) {
        if (err) throw err;
        console.log(res.body);
  });

Apart from the parameters for request(), the remainder of the code is the same. As we continue through this article, we’ll demonstrate SuperTest with the Dog API. You can later adapt these tests for your APIs.

Setting Response Expectations

In the previous section, you’ve seen how to create an HTTP request. Until here, however, you only used inherited superagent functionality. Making an HTTP request is not yet the same thing as testing it. You need to set your expectations for a functional API test and confirm that the API responds as expected. To validate that, SuperTest extends superagent with the expect() method. This versatile method allows you to test several things about the API response.

HTTP APIs return various status codes. Codes in the 4xx range indicate client errors, and codes in the 5xx range indicate server errors. For a successful API request, you generally expect 200. Sometimes it’s 201 or 204 instead (you’d find this information in the API documentation). Let’s extend our test request with the expectation that it returns 200:

request('https://dog.ceo')
  .get('/api/breeds/image/random')
  .expect(200)
  .end(function(err, res) {
    if (err) throw err;
  });

As you can see, expect() calls always come before end().

Most contemporary HTTP APIs return JSON responses. The Dog API is no exception. For these responses, the value of the Content-Type header should be application/json. Let’s add another expectation:

request('https://dog.ceo')
  .get('/api/breeds/image/random')
  .expect(200)
  .expect('Content-Type', 'application/json')
  .end(function(err, res) {
    if (err) throw err;
  });

The code above should run successfully without throwing exceptions. You can try to expect() something different to see how the test fails.

Checking the Response Bodies

A common mistake with functional API testing is that developers only check for HTTP status codes and headers. Such shallow testing is called smoke testing. It helps establish the bare minimum expectations that the API is up and responds at all, but it’s essential to check for response bodies, too. API consumers expect specific properties in the JSON object, so those should be present. The Dog API returns a message and a status property, so let’s check whether those exist:

request('https://dog.ceo')
  .get('/api/breeds/image/random')
  .expect(200)
  .expect('Content-Type', 'application/json')
  .expect(function(res) {
    if (!res.body.hasOwnProperty('status')) throw new Error("Expected 'status' key!");
    if (!res.body.hasOwnProperty('message')) throw new Error("Expected 'message' key!");
  })
  .end(function(err, res) {
    if (err) throw err;
  });

As you can see, we can call expect() with a callback function and write code that checks the response. That is the one approach to analyzing a JSON response. It looks a little lengthy, though. One way to improve this code is by using the assert() function. You need to add a “require” statement to use assert, but you don’t have to install anything because it’s a Node.js built-in module.

const request = require('supertest');
const assert = require('assert');

request('https://dog.ceo')
  .get('/api/breeds/image/random')
  .expect(200)
  .expect('Content-Type', 'application/json')
  .expect(function(res) {
    assert(res.body.hasOwnProperty('status'));
    assert(res.body.hasOwnProperty('message'));
  })
  .end(function(err, res) {
    if (err) throw err;
  });

Of course, you can use other assertion libraries, such as Chai.js. There are also different ways to check response bodies built into SuperTest. You can pass a string or regular expression to the expect() method. A string must match the whole body, whereas you can use a regular expression to review parts of it. Here’s another example:

request('https://dog.ceo')
  .get('/api/breeds/image/random')
  .expect(200)
  .expect('Content-Type', 'application/json')
  .expect(/{"message":".*","status":"success"}/)
  .end(function(err, res) {
    if (err) throw err;
  });

Combining SuperTest and Mocha

You can write SuperTest code inside the Mocha test framework. You can use describe() and it() to structure your tests. If you’ve used Mocha before, you should be familiar with these methods. Then, inside it(), you use the same request code as we’ve discussed before. There’s just one specific difference: you must drop the end(). Instead, add Mocha’s done to the last expect() call as an additional parameter. Here’s an example:

describe('Random Dog Image', function() {
  it('responds with expected JSON structure', function(done) {
    request('https://dog.ceo')
      .get('/api/breeds/image/random')
      .expect(200)
      .expect('Content-Type', 'application/json')
      .expect(/{"message":".*","status":"success"}/, done);
  });
});

Designing Functional Tests

Now that you know the basics of testing APIs let’s quickly talk about what you should test. For internal APIs, you should always test all endpoints that you expose. As we’ve done in the examples above, you can write “unit tests” for one endpoint. You can also chain multiple API calls to test workflows involving numerous API calls. It’s also a great idea not just to try the “happy path” but also to call some non-existing endpoints or send invalid data and expect() the desired error message. For external APIs, you can test all endpoints that you’re consuming to know you can rely on them.

Wrapping Up

This article should help you get started with your first API test with SuperTest, but it’s by no means exhaustive. Here are a few other resources from around the web that you can use to learn more:

API testing is one part of a complete test strategy. Apart from API testing, you should do end-to-end tests involving your application’s user interface and the respective APIs. With Testim, you have a whole platform for end-to-end testing both in your browser and in the cloud, and Testim provides the ability to run API tests as part of your UI tests.

What to read next

API Testing: A Developer’s Tutorial and Complete Guide

The 10 API Testing Tools You Can’t Live Without in 2021