How to Test Express APIs With Jest

Testing, while it can be time-consuming, is an important step in the development cycle of any application. It ensures you catch bugs and issues early on before you push code to production.
You can use Jest to test an Express Rest API. After you’ve created a simple CRUD API, discover how to write tests for each endpoint.
What Is Jest?
There are many JavaScript testing libraries that you can choose from, but Jest is the easiest to start with. It is a testing library developed by Facebook, mostly used to test React projects. However, you can also use it to test Node and other JavaScript-based projects. It was developed on top of Jasmine, another testing tool, and comes bundled with its own assertion library.
While you won’t need an assertion library to write tests in Jest, you will need to use a tool to make HTTP requests. This article uses SuperTest.
What Is SuperTest?
SuperTest is a Node testing library for HTTP calls. It extends the superagent testing library and allows you to make requests like GET, POST, PUT, and DELETE.
SuperTest provides a request object you can use to make HTTP requests.
const request = require("supertest")
request("https://icanhazdadjoke.com")
.get('/slack')
.end(function(err, res) {
if (err) throw err;
console.log(res.body.attachments);
});
Here, you pass the base URL of the API to the request object and then chain the HTTP method with the rest of the URL. The end() method calls the API server and the callback function handles its response.
Once you get the response from the API, you can use Jest to validate it.
Create an Express API
To test your own API endpoints, you need to create a REST API first. The API you will create is quite simple. It inserts, retrieves, updates, and deletes items from an array.
Begin by creating a new directory called node-jest and initializing npm.
mkdir node-jest
npm init -y
Next, create a new file called index.js and create the Express server.
const express = require("express")
const app = express()
app.listen(3000, () => console.log("Listening at port 3000"))
Test the GET /todos Endpoint
The first endpoint you will create is the GET /todos endpoint. It returns all the items in the array. In index.js, add the following.
const todos = [
];
// Get all todos
app.get("/todos", (req, res) => {
return res.status(200).json({
data: todos,
error: null,
});
});
Note the response has a status code of 200 and a JSON object containing the to-dos item in an array called data and an error message. This is what you will test using Jest.
Now, install Jest and SuperTest:
npm install jest supertest
Then, add a test script in package.json as follows:
{
"scripts": {
"test": "jest"
}
}
Before you start writing your own tests, you should understand how to write a basic test in Jest.
Consider the following function:
function sum(a, b) {
return a + b;
}module.exports = sum;
In the test file, you need to:
- Import the function.
- Describe what the test should do.
- Call the function.
- Assert the expected response with the actual response from the function.
const { sum } = require("./sum")describe("Sum of two items", async() => {
test("It should return 4", () => {
expect(sum(2,2)).toBe(4)
})
})
The describe keyword specifies the group of tests and the test statement specifies the specific test. If the value returned from the function matches the value passed to toBe, the test passes.
When testing API endpoints, you won’t be calling a function but sending a request using SuperTest or another HTTP client library.
Returning to the GET endpoint, create a new file called api.test.js. This is where you will write all the endpoint tests. Naming the test file with a .test infix ensures that Jest recognizes it as a test file.
In api.test.js, import supertest and set the base URL like so:
const request = require("supertest")
const baseURL = "http://localhost:3000"
Next, create the first test in the describe block:
describe("GET /todos", () => {
const newTodo = {
id: crypto.randomUUID(),
item: "Drink water",
completed: false,
}
beforeAll(async () => {
// set up the todo
await request(baseURL).post("/todo").send(newTodo);
})
afterAll(async () => {
await request(baseURL).delete(`/todo/${newTodo.id}`)
})
it("should return 200", async () => {
const response = await request(baseURL).get("/todos");
expect(response.statusCode).toBe(200);
expect(response.body.error).toBe(null);
});
it("should return todos", async () => {
const response = await request(baseURL).get("/todos");
expect(response.body.data.length >= 1).toBe(true);
});
});
Before running the tests, you will need to define setup and teardown functions. These functions will populate the todo array with an item before the test and delete the dummy data after each test.
The code that runs before all the tests is in the beforeAll() function. The code that runs after all the tests is in the afterAll() function.
In this example, you are simply hitting the POST and DELETE endpoints for each. In a real application, you would probably connect to a mock database containing the test data.
In this test, you first made a request to the GET /todos endpoint and compared the response sent back to the expected results. This test suite will pass if the response has an HTTP status code of 200, the data is not empty, and the error message is null.
Test the POST /todo Endpoint
In index.js, create the POST /todo endpoint:
app.post("/todo", (req, res) => {
try {
const { id, item, completed } = req.body;
const newTodo = {
id,
item,
completed,
};
todos.push(newTodo);
return res.status(201).json({
data: todos,
error: null,
});
} catch (error) {
return res.status(500).json({
data: null,
error: error,
});
}
});
In this test, you will need to send the todo details in the request body using the send() method.
request(baseURL).post("/todo").send(newTodo)
The POST /todo request should return a 201 status code and the todos array with the new item added at the end. Here is what the test might look like:
describe("POST /todo", () => {
const newTodo = {
// todo
}
afterAll(async () => {
await request(baseURL).delete(`/todo/${newTodo.id}`)
})
it("should add an item to todos array", async () => {
const response = await request(baseURL).post("/todo").send(newTodo);
const lastItem = response.body.data[response.body.data.length-1]
expect(response.statusCode).toBe(201);
expect(lastItem.item).toBe(newTodo["item"]);
expect(lastItem.completed).toBe(newTodo["completed"]);
});
});
Here, you are passing the todo data to the send() method as an argument. The response should have a 201 status code and also contain all the todo items in a data object. To test whether todo was actually created, check whether the last entry in the returned todos matches the one you sent in the request.
The PUT /todos/:id endpoint should return the updated item:
app.put("/todos/:id", (req, res) => {
try {
const id = req.params.id
const todo = todos.find((todo) => todo.id == id);
if(!todo) {
throw new Error("Todo not found")
}
todo.completed = req.body.completed;
return res.status(201).json({
data: todo,
error: null,
});
} catch (error) {
return res.status(500).json({
data: null,
error: error,
});
}
});
Test the response as follows:
describe("Update one todo", () => {
const newTodo = {
// todo
}
beforeAll(async () => {
await request(baseURL).post("/todo").send(newTodo);
})
afterAll(async () => {
await request(baseURL).delete(`/todo/${newTodo.id}`)
})
it("should update item if it exists", async () => {
const response = await request(baseURL).put(`/todos/${newTodo.id}`).send({
completed: true,
});
expect(response.statusCode).toBe(201);
expect(response.body.data.completed).toBe(true);
});
});
The completed value in the response body should be true. Remember to include the id of the item you want to update in the URL.
Test the DELETE /todos/:id Endpoint
In index.js, create the DELETE endpoint. It should return the todo data without the deleted item.
app.delete("/todos/:id", (req, res) => {
try {
const id = req.params.id
const todo = todos[0]
if(todo) {
todos.splice(id, 1)
}
return res.status(200).json({
data: todos,
error: null,
});
} catch (error) {
return res.status(500).json({
data: null,
error: error,
});
}
});
To test the endpoint, you can check whether the deleted item still exists in the returned data:
describe("Delete one todo", () => {
const newTodo = {
// todo
}
beforeAll(async () => {
await request(baseURL).post("/todo").send(newTodo);
})
it("should delete one item", async () => {
const response = await request(baseURL).delete(`/todos/${newTodo.id}`);
const todos = response.body.data
const exists = todos.find(todo => {
newTodo.id == todoId
})
expect(exists).toBe(undefined)
});
});
The data returned from the DELETE endpoint should not contain the deleted item. Since the returned items are in an array, you can use Array[id] to check whether the API deleted the item correctly. The result should be false.
Creating REST APIs
In this article, you learned how to test an Express Rest API using Jest API. You wrote tests for the GET, PUT, POST, and DELETE HTTP requests and saw how to send data to the endpoint in the URL and the request. You should be able to apply this knowledge when testing your own Rest API.
ncG1vNJzZmivp6x7rq3KnqysnZ%2Bbe6S7zGicsaiimsC0ecCpoKxlmprAtXnTnqqtZw%3D%3D