Testing Express Api with Jest and Supertest

One of the ways to test our applications is using tools like Insomnia, Postman or even through Swagger. However, this entire process is time consuming, we do not always test our entire application whenever we make any changes to our Api.

This is one of the many reasons why automated testing is ideal. So I decided to write this article to present you with a simple and easy-to-understand example, which has a process very similar to what you would be used to (with Insomnia, etc).

So we're going to use two libraries that I love, a testing framework called Jest and the other is a library for doing http testing, called supertest.

And with that we are going to apply a concept called Behavioral Testing, that is, the tests we are going to perform will not have knowledge of the internal structure of our Api, everything we are going to test has to do with the input and output of the data.

The idea of today's application is to add emails to a database (actually it's an array of data that it is in memory), which already has a complete CRUD. All we need to do is test the behavior of those same endpoints.

Let's code

We will need to install the following dependencies:

npm i express

# dev dependencies

npm i -D jest supertest

Now let's pretend that our app.js looks like this:

const express = require("express");

const app = express();

app.use(express.json());

const fakeDB = [
  {
    id: Math.floor(Math.random() * 100),
    email: "[email protected]",
  },
];

app.get("/", (req, res) => {
  return res.status(200).json({ data: fakeDB });
});

app.post("/send", (req, res) => {
  fakeDB.push({
    id: Math.floor(Math.random() * 100),
    email: req.body.email,
  });
  return res.status(201).json({ data: fakeDB });
});

app.put("/update/:id", (req, res) => {
  const obj = fakeDB.find((el) => el.id === Number(req.params.id));
  obj.email = req.body.email;
  return res.status(200).json({ data: fakeDB });
});

app.delete("/destroy/:id", (req, res) => {
  const i = fakeDB.findIndex((el) => el.id === Number(req.params.id));
  fakeDB.splice(i, 1);
  return res.status(200).json({ data: fakeDB });
});

module.exports = app;

And that in our main.js is the following:

const app = require("./app");

const start = (port) => {
  try {
    app.listen(port, () => {
      console.log(`Api running at http://localhost:${port}`);
    });
  } catch (err) {
    console.error(err);
    process.exit();
  }
};

start(3333);

Now that we have our Api, we can start working on testing our application. Now in our package.json, in the scripts property, let's change the value of the test property. For the following:

"scripts": {
    "start": "node main",
    "test": "jest"
 },

This is because we want Jest to run our application tests. So we can already create a file called app.test.js, where we will perform all the tests we have in our app.js module.

First we will import the supertest and then our app.js module.

const request = require("supertest");

const app = require("./app");

// More things come after this

Before we start doing our tests, I'm going to give a brief introduction to two functions of Jest that are fundamental.

The first function is describe(), which groups together a set of individual tests related to it.

And the second is test() or it() (both do the same, but to be more intuitive in this example I'm going to use test()), which performs an individual test.

First let's create our test group, giving it the name of Test example.

const request = require("supertest");

const app = require("./app");

describe("Test example", () => {
  // More things come here
});

Now we can focus on verifying that when we access the main route ("/") using the GET method, we get the data that is stored in our database. First let's create our individual test, giving it the name GET /.

describe("Test example", () => {
  test("GET /", (done) => {
    // Logic goes here
  });
  // More things come here
});

Now we can start using supertest and one of the things I start by saying is super intuitive. This is because we can make a chain of the process.

First we have to pass our app.js module in to be able to make a request, then we define the route, what is the content type of the response and the status code.

describe("Test example", () => {
  test("GET /", (done) => {
    request(app)
      .get("/")
      .expect("Content-Type", /json/)
      .expect(200)
      // More logic goes here
  });
  // More things come here
});

Now we can start looking at the data coming from the response body. In this case we know that we are going to receive an array of data with a length of 1 and that the email of the first and only element is [email protected].

describe("Test example", () => {
  test("GET /", (done) => {
    request(app)
      .get("/")
      .expect("Content-Type", /json/)
      .expect(200)
      .expect((res) => {
        res.body.data.length = 1;
        res.body.data[0].email = "[email protected]";
      })
      // Even more logic goes here
  });
  // More things come here
});

Then, just check if there was an error in the order, otherwise the individual test is finished.

describe("Test example", () => {
  test("GET /", (done) => {
    request(app)
      .get("/")
      .expect("Content-Type", /json/)
      .expect(200)
      .expect((res) => {
        res.body.data.length = 1;
        res.body.data[0].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        return done();
      });
  });
  // More things come here
});

Basically this is the basis for many others, however we have only tested one of the routes yet, so now we need to test if we can insert data into the database.

So we're going to create a new test called POST /send, but this time we're going to change the route as well as the method.

describe("Test example", () => {
  // Hidden for simplicity
  test("POST /send", (done) => {
    request(app)
      .post("/send")
      .expect("Content-Type", /json/)
      // More logic goes here
  });
  // More things come here
});

Now we have to send a JSON body with just one property called email and we know the status code is going to be 201.

describe("Test example", () => {
  // Hidden for simplicity
  test("POST /send", (done) => {
    request(app)
      .post("/send")
      .expect("Content-Type", /json/)
      .send({
        email: "[email protected]",
      })
      .expect(201)
      // Even more logic goes here
  });
  // More things come here
});

Now we can check the body of the response, as a new element has been added to the database we know that the length is now two and that the email of the first element must be the initial one and that of the second element must be the same as the one sent .

describe("Test example", () => {
  // Hidden for simplicity
  test("POST /send", (done) => {
    request(app)
      .post("/send")
      .expect("Content-Type", /json/)
      .send({
        email: "[email protected]",
      })
      .expect(201)
      .expect((res) => {
        res.body.data.length = 2;
        res.body.data[0].email = "[email protected]";
        res.body.data[1].email = "[email protected]";
      })
      // Almost done
  });
  // More things come here
});

And let's check if an error occurred during the execution of the order, otherwise it's finished. But this time we are going to create a variable to add the id of the second element, so that we can dynamically update and delete it afterwards.

let elementId;

describe("Test example", () => {
  // Hidden for simplicity
  test("POST /send", (done) => {
    request(app)
      .post("/send")
      .expect("Content-Type", /json/)
      .send({
        email: "[email protected]",
      })
      .expect(201)
      .expect((res) => {
        res.body.data.length = 2;
        res.body.data[0].email = "[email protected]";
        res.body.data[1].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        elementId = res.body.data[1].id;
        return done();
      });
  });
  // More things come here
});

Now we are going to update an element that was inserted in the database, in this case we are going to use the id that we have stored in the elementId variable. Later we will create a new test, we will define a new route and we will use another http method.

describe("Test example", () => {
  // Hidden for simplicity
  test("PUT /update/:id", (done) => {
    request(app)
      request(app)
      .put(`/update/${elementId}`)
      .expect("Content-Type", /json/)
      // More logic goes here
  });
  // More things come here
});

In this endpoint we will also send in the JSON body a property called email, however this time we will use another one, as we expect the status code to be 200.

describe("Test example", () => {
  // Hidden for simplicity
  test("PUT /update/:id", (done) => {
    request(app)
      request(app)
      .put(`/update/${elementId}`)
      .expect("Content-Type", /json/)
      .send({
        email: "[email protected]",
      })
      .expect(200)
      // Even more logic goes here
  });
  // More things come here
});

In the response code we expect the length of the array to be 2 and that this time the second element must have the value of the new email that was sent.

describe("Test example", () => {
  // Hidden for simplicity
  test("PUT /update/:id", (done) => {
    request(app)
      request(app)
      .put(`/update/${elementId}`)
      .expect("Content-Type", /json/)
      .send({
        email: "[email protected]",
      })
      .expect(200)
      .expect((res) => {
        res.body.data.length = 2;
        res.body.data[0].email = "[email protected]";
        res.body.data[1].id = elementId;
        res.body.data[1].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        return done();
      });
  });
  // More things come here
});

Last but not least, let's try to eliminate the element from our database that has the id with the same value as the elementId variable.

The process is similar to what was done in the previous test. But of course, let's define a new route and let's use the appropriate http method.

describe("Test example", () => {
  // Hidden for simplicity
  test("DELETE /destroy/:id", (done) => {
    request(app)
      .delete(`/destroy/${elementId}`)
      .expect("Content-Type", /json/)
      .expect(200)
      // More logic goes here
  });
});

Now when looking at the response body, this time the array length value should be 1 and the first and only element should be the initial email.

describe("Test example", () => {
  // Hidden for simplicity
  test("DELETE /destroy/:id", (done) => {
    request(app)
      .delete(`/destroy/${elementId}`)
      .expect("Content-Type", /json/)
      .expect(200)
      .expect((res) => {
        res.body.data.length = 1;
        res.body.data[0].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        return done();
      });
  });
});

The test file (app.test.js) should look like this:

const request = require("supertest");

const app = require("./app");

let elementId;

describe("Test example", () => {
  test("GET /", (done) => {
    request(app)
      .get("/")
      .expect("Content-Type", /json/)
      .expect(200)
      .expect((res) => {
        res.body.data.length = 1;
        res.body.data[0].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        return done();
      });
  });

  test("POST /send", (done) => {
    request(app)
      .post("/send")
      .expect("Content-Type", /json/)
      .send({
        email: "[email protected]",
      })
      .expect(201)
      .expect((res) => {
        res.body.data.length = 2;
        res.body.data[0].email = "[email protected]";
        res.body.data[1].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        elementId = res.body.data[1].id;
        return done();
      });
  });

  test("PUT /update/:id", (done) => {
    request(app)
      .put(`/update/${elementId}`)
      .expect("Content-Type", /json/)
      .send({
        email: "[email protected]",
      })
      .expect(200)
      .expect((res) => {
        res.body.data.length = 2;
        res.body.data[0].email = "[email protected]";
        res.body.data[1].id = elementId;
        res.body.data[1].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        return done();
      });
  });

  test("DELETE /destroy/:id", (done) => {
    request(app)
      .delete(`/destroy/${elementId}`)
      .expect("Content-Type", /json/)
      .expect(200)
      .expect((res) => {
        res.body.data.length = 1;
        res.body.data[0].email = "[email protected]";
      })
      .end((err, res) => {
        if (err) return done(err);
        return done();
      });
  });
});

Now when you run the npm test command in the terminal, you should get a result similar to this:

Conclusion

I hope it was brief and that you understood things clearly. In the beginning I wasn't a big fan of automated tests but now I practically can't live without them. 🤣

Have a nice day! 👏 ☺️

11