API Testing with Jest Mocks and SuperTest
One of the main challenges in doing API testing is database dependency. In the last article Repository Pattern in JavaScript we looked at how to abstract data access operations. One of the main characteristics of the pattern is Testability: The Repository Pattern facilitates unit testing because you can easily replace the actual data access logic with mock data for testing purposes. In this article we will look at how to use Jest mocks to replace the data we get from the repository.
How to Do API Testing in Your Project
Please watch the above video for more details on API Testing implementation.
In order to replace data from the repository, we will need to create test data, or fixtures. Before we do that, let’s first update jest-presets.js
file in the root folder of our project to exclude fixtures folder from the tests by putting the following line of code in modulePathIgnorePatterns
"<rootDir>/src/__tests__/__fixtures__",
Level up your testing game with Testing JavaScript Applications! Learn how to write robust tests using tools like Jest and build reliable, maintainable code. Get your copy today and start mastering testing! 👉 https://amzn.to/4exDckW
Now let’s create fixtures for travels and tours in the __tests__/__fixtures__
folder. We will be sure that the exported constants tour and travel have types TourAttributes and TravelAttributes respectively, the same types that are returned from the repository methods.
// tours.ts
export const tour: TourAttributes = {
id: "f55946c0-df6d-4898-a9e4-6bf01b8dad42",
travel_id: "c85946c0-df6d-4898-a9e4-6bf01b8dad43",
name: "My great tour",
starting_date: new Date(),
ending_date: new Date(),
price: 5.0,
created_at: new Date(),
updated_at: new Date(),
};
// travels.ts
import { tour } from "./tours";
export const travel: TravelAttributes = {
id: "12345",
name: "My Travel",
description: "This is my travel",
slug: "my-travel",
number_of_days: 3,
tours: [tour],
is_public: true,
created_at: new Date(),
updated_at: new Date(),
};
Now let’s go ahead and test travels. Let’s create travels.test.ts
file in __tests__
folder and put the following code:
import supertest from "supertest";
import { createServer } from "../server";
import TravelRepository from "../repositories/TravelRepository";
import { travel } from "./__fixtures__/travels";
import { ConnectionRefusedError } from "sequelize";
jest.mock("../repositories/TravelRepository");
const getAll = jest.fn();
const getById = jest.fn();
//@ts-ignore
TravelRepository.mockImplementation(() => {
return {
getAll,
getById,
};
});
beforeEach(() => {
// @ts-ignore
TravelRepository.mockClear();
getAll.mockClear();
getById.mockClear();
});
In the above code we imported TravelRepository
and travels
fixture. We used Jest mock to mock the repository and defined getAll
and getById
methods as Jest functions that return undefined (later on in the tests we will create specific implementations for them). Most importantly we have beforeEach
function to clear the mocks before each test.
Let’s continue with travels.test.ts
file by adding the code for the tests:
describe("list travels endpoint", () => {
it("it returns travels", async () => {
getAll.mockImplementation(
async (
options?: Record<string, any>
): Promise<Array<TravelAttributes>> => {
return [travel];
}
);
await supertest(createServer())
.get("/v1/travels")
.expect(200)
.then((res) => {
expect(res.body.travels.length).toBe(1);
expect(res.body.travels[0]).not.toHaveProperty("created_at");
expect(res.body.travels[0].tours.length).toBe(1);
});
});
it("it returns no travels", async () => {
getAll.mockImplementation(
async (
options?: Record<string, any>
): Promise<Array<TravelAttributes>> => {
return [];
}
);
await supertest(createServer())
.get("/v1/travels")
.expect(200)
.then((res) => {
expect(res.body.travels.length).toBe(0);
});
});
it("throws and an error when getting travels", async () => {
const errorMessage = "Connection refused";
getAll.mockImplementation(
async (
options?: Record<string, any>
): Promise<Array<TravelAttributes>> => {
const parentError = new Error(errorMessage);
throw new ConnectionRefusedError(parentError);
}
);
await supertest(createServer())
.get("/v1/travels")
.expect(400)
.then((res) => {
expect(res.body.error.message).toBe(errorMessage);
});
});
});
describe("get travel endpoint", () => {
it("it returns a travel", async () => {
getById.mockImplementation(
async (
id: string,
options?: Record<string, any>
): Promise<TravelAttributes> => {
return travel;
}
);
await supertest(createServer())
.get("/v1/travels/abcd")
.expect(200)
.then((res) => {
expect(res.body).toHaveProperty("travel");
expect(res.body.travel).not.toHaveProperty("created_at");
});
});
it("it returns 404 not found", async () => {
getById.mockImplementation(
async (
id: string,
options?: Record<string, any>
): Promise<TravelAttributes | null> => {
return null;
}
);
await supertest(createServer())
.get("/v1/travels/abcd")
.expect(404)
.then((res) => {
expect(res.body).toHaveProperty("error");
expect(res.body.error.code).toBe("ERR_NF");
});
});
});
In the above code we created a few tests to test GET /v1/travels and GET /v1/travels/{id} routes. We use SuperTest library to make calls to those endpoints. SuperTest allows to make API calls to our API without us spinning a server by running yarn dev
command. SuperTest also provides convenient assertion methods like expecting a certain status code from the response. Please, read SuperTest documentation to learn about features and available assertions.
Conclusion
This is how you can test your API using Jest Mocks and SuperTest. We hope it gives you a good primer so you can write tests for your own API. Happy Coding!