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.

API Testing with Jest Mocs

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__",

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!

References

Share this article

Posted

in

by