How To Test Your Rest API With Jest And SuperTest (I)

Olaoluwa
FAUN — Developer Community 🐾
8 min readJul 7, 2022

--

Hello 👋 and welcome, I’m undergoing a refinery process with The Startup Intern. During this process, I’ll be provided with study resources and tasks that will help solidify my learning. On successful completion of this process, I’ll be assigned to an organization as a backend developer intern. Kindly check out The Startup Intern on their website or on Twitter to know more about them and what they do. 🤝

This article is sequel to; “How To Setup Your TypeScript App Test Environment With Jest”, where I discussed how to setup your test environment by installing the jest framework and its dependencies. In this article, I will discuss how to perform an integrated test on your REST API with SuperTest and Jest.

I assume that you are already familiar with how to do a unit test with the jest framework. If not so, to get up to speed, please visit my previous article on how to set up your test environment and then these tutorials (Jest Mathchers and Hooks) on softwaretestinghelp.com.

At the end of this article, you should be able to:

What is SuperTest?

Testing HTTP request with Jest alone is impossible. SuperTest makes HTTP assertions easier. It is a library that enables developers to test HTTP requests such as GET, POST, PATCH, PUT and DELETE. It is built on top of Superagent a Node and browser HTTP client.

How To Install SuperTest & Prepare Your Environment.

After setting up your test environment as discussed in the previous article, install SuperTest and its type definitions as seen in the commands below respectively:

npm i --save dev supertestnpm i --save dev @types/supertest

N.B: If you are not using typeScript you do not need to install the types definition.

Preparing/configuring your environment for integration tests may vary. It simply depends on the setup of your project. But the below steps will give you a head start:

  1. Do not listen to connections: such as the server in your application when performing an integration test. So whenever you are running an npm command to test your application do not start the server. You can achieve that by prompting Node to skip server connection in the ./app.ts or ./server.ts file when the Node environment is set to “test” (lines 5–9).
> app.ts1 const PORT: number = Number(process.env.PORT) || 7000;
2
3 let server: Server;
4
5 if (process.env.NODE_ENV !== "test") {
6 server = app.listen(PORT, () => {
7 console.log(`App now listening on port ${PORT}`);
8 });
9 }

Also, configure your database connection such that when the node environment is set to “test”, no database collection should be created
(line 8–17).

> models/db_config/db.config.ts1 import { connect } from "mongoose";
2
3 import * as dotenv from "dotenv";
4 dotenv.config();
5
6 let URI: string = String(process.env.MONGO_URI);
7
8 if (process.env.NODE_ENV !== "test") {
9 connect(URI)
10 .then(() => {
11 console.log(`Connected to database`);
12 })
13 .catch((error) => {
14 console.error(`Couldn't connect to database`, error);
15 process.exit(1);
16 });
17 }

2. Set a test database URI: since the application connections (sever and database) will not be in use for the integration test, it is needed to create a test database since the test depends on it. The first step is to set the test database URI in the config environment file (line 3).

> .env1 NODE_ENV='test'
2 MONGO_URI_TEST='mongodb://localhost:27017/article'
3 MONGO_URI_TEST='mongodb://localhost:27017/article_test'

How To Perform A Simple Integration Test (GET methods)

To perform an integration test on a rest api that depends on an external database, there is a need for the app.ts file and jest before and after hooks. The app.ts file is needed because it contains all the application setups such as middlewares, controllers, and routes. While the before and after hooks contain the scripts that should be executed by jest before and after the test case(s) respectively.

1: Before & After Hooks

> src/tests/integration_tests/article.test.ts1 import request from "supertest";
2
3 import mongoose from "mongoose";
4 import { app } from "../../app";
5
6
7 describe("ARTICLES", () => {
8 beforeEach(async () => {
9 try {
10 await mongoose.connect(String(process.env.MONGO_URI_TEST));
11 console.log("1: test database connected");
12 } catch (error: any) {
13 console.log(error.message);
14 }
15 });
16
17 afterEach(async () => {
18 console.log(`3: first test case completed`);
19 await mongoose.disconnect();
20 await mongoose.connection.close();
21 });
22
23 describe("TEST /", () => {
24 it("should log me after the before hook", async () =>
25 {
26 console.log(`2: first test case`)
27 });
28 });
29 });

The scripts above instruct jest to connect to a database before each test case (lines 8–15) and disconnect the database after each test case (lines 17–21). If you execute the test script, the output should be in the same order as the image below:

npm test

2: Involving SuperTest

> src/tests/integration_tests/article.test.ts1 import request from "supertest";
2
3 import mongoose from "mongoose";
4 import { app } from "../../app";
5
6 const req = request(app);
7
8 describe("ARTICLES", () => {
9 beforeEach(async () => {
10 try {
11 await mongoose.connect(String(process.env.MONGO_URI_TEST));
12 } catch (error: any) {
13 console.log(error.message);
14 }
15 });
16
17 afterEach(async () => {
18 await mongoose.disconnect();
19 await mongoose.connection.close();
20 });
21
22 describe("GET /", () => {
23 it("should return all articles", async () => {
24 const res = await req.get("/api/v1/mobile/articles");
25 expect(res.status).toBe(200);
26 });
27 });
21 });

using superTest (lines 1& 6) makes testing HTTP requests possible. The scripts above (lines 22–26) make a request to get all routes in the database and should return a response status of 200 OK. If the response status is otherwise, it will become a failing test.

npm test

At this point, the article collection is empty but the request was successful. It returns an empty document and response status of 200 OK.

Example GET /id

This example illustrates how to test for a GET /id request method from a database collection. Document(s) will be added to the empty collection (article collection), and it is vital to instruct jest to delete all documents in the collection after each tests cases in order to avoid conflicts. This would be done in the afterEach hook like so:

> src/tests/integration_tests/article.test.tsimport Article from "../../resources/models/article.model";afterEach(async () => {
await Article.deleteMany({});
await mongoose.disconnect();
await mongoose.connection.close();
});

When performing a test (either unit or integrated), the number of test cases should equate to or be more than the number of execution paths. During the implementation of the GET /id request for this example, there are three execution paths. Below are the execution paths and how to test them:

  • the request should return a status 400 BAD REQUEST if the “id” is invalid.
> src/tests/integration_tests/article.test.tsdescribe("GET /id", () => {
it("should return a 400 status if '_id' is invalid", async () => {
// set id to unmathch ObjectId
const res = await req.get("/api/v1/mobile/articles/6");
expect(res.status).toBe(400);
});
});
  • the request should return a status 404 NOT FOUND if the “id” is valid but not found in the database.
> src/tests/integration_tests/article.test.tsdescribe("GET /id", () => {
it("should return a 404 status if article is not found", async () => {
// set query id to a new id not in db
const res = await req.get(`/api/v1/mobile/articles/${new
mongoose.Types.ObjectId()}`);
expect(res.status).toBe(400);
});
});
  • the request should return a status 200 OK if the “id” is valid and found in the database then return the article document.
> src/tests/helpers/article.test.tsconst testArticle = {
title: "test title",
category: "internet",
body: "not an empty body",
};
export default testArticle;

In this execution path, an article will be created before the request so there can be a valid id to test with. To keep things clean, a testArticle object (code above) was created inside the helpers folder.

> src/tests/integration_tests/article.test.tsimport testArticle from "../helpers/article.test.helper";describe("GET /id", () => {
it("should retun an article if '_id' is valid", async () => {
// add article to the collection
const newArticle = await Article.create(testArticle);
const res = await
req.get(`/api/v1/mobile/articles/${newArticle._id}`);
const article = res.body.data; expect(res.status).toBe(200);
expect(article).toHaveProperty("title", newArticle.title);
});
});

Bringing all test cases together and executing them would result in passing tests.

> src/tests/integration_tests/article.test.ts1 import request from "supertest";
2
3 import mongoose from "mongoose";
4 import { app } from "../../app";
5 import Article from "../../resources/models/article.model";
6 import testArticle from "../helpers/article.test.helper";
7
8 const req = request(app);
9
10 describe("ARTICLES", () => {
11 beforeEach(async () => {
12 try {
13 await mongoose.connect(String(process.env.MONGO_URI_TEST));
14 } catch (error: any) {
15 console.log(error.message);
16 }
17 });
18
19 afterEach(async () => {
20 await Article.deleteMany({});
21 await mongoose.disconnect();
22 await mongoose.connection.close();
23 });
24
25 describe("GET /", () => {
26 it("should return all articles", async () => {
27 const res = await req.get("/api/v1/mobile/articles");
28 expect(res.status).toBe(200);
29 });
30 });
31 });
32
33 describe("GET /id", () => {
34 it("should return a 400 status if '_id' is invalid", async ()
35 => {
36 // set id to unmathch ObjectId
37 const res = await req.get("/api/v1/mobile/articles/6");
38 expect(res.status).toBe(400);
39 });
40
41 it("should return a 404 status if article is not found", async()
42 => {
43 // set query id to a new id not in db
44 const res = await req.get(`/api/v1/mobile/articles/${new
45 mongoose.Types.ObjectId()}`);
46 expect(res.status).toBe(400);
47 });
48
49 it("should retun an article if '_id' is valid", async () => {
50 // add article to the collection
51 const newArticle = await Article.create(testArticle);
52
53 const res = await
54 req.get(`/api/v1/mobile/articles/${newArticle._id}`);
55
56 const article = res.body.data;
57
58 expect(res.status).toBe(200);
59 expect(article).toHaveProperty("title", newArticle.title);
60 });
61 });
62 }};
npm test

I hope you find this article helpful. Please watch out for the second part where I will discuss how to perform integrated tests with SperTest using other HTTP methods.

👋

Resources

If this post was helpful, please click the clap 👏 button below a few times to show your support for the author 👇

🚀Developers: Learn and grow by keeping up with what matters, JOIN FAUN.

--

--