Library-Agnostic Mocks for HTTP Request Libraries in Node.js

giraldidev

Leonardo Giraldi Moreno Giuranno

Posted on October 23, 2023

Library-Agnostic Mocks for HTTP Request Libraries in Node.js

Since I started implementing unit tests for the projects and applications I was involved in, I used to set up the mock for HTTP request libraries individually. For instance, if I was using axios to make calls to other APIs, during test implementation, I would configure the axios mock itself. Let's take a look at this implementation:

Consider the following function:

const axios = require("axios");

async function fetchCep(cep) {
        const { data } = await axios.get(`https://viacep.com.br/ws/${cep}/json/`);
    return data;
}

module.exports = {
      fetchCep,
};
Enter fullscreen mode Exit fullscreen mode

The simple function fetchCep(cep) uses the axios library to make an HTTP GET request to a ZIP code lookup service provided by the ViaCEP API. The purpose of this function is to retrieve address information based on a provided ZIP code as an argument.

Now, let's implement the unit test that validates whether the fetchCep function returns the provided ZIP code information obtained from the ViaCEP API:

const mockAxiosGet = jest.fn();
jest.mock("axios", () => ({
    get: mockAxiosGet,
}));

const { fetchCep } = require("./axiosMockExample");

describe("fetchCep", () => {
    it("should return cep information", async () => {
        mockAxiosGet.mockResolvedValueOnce({
            data: {
                cep: "12345678",
                logradouro: "Avenida Paulista",
                bairro: "Centro",
                localidade: "São Paulo",
                uf: "SP",
            },
        });
        const cep = "12345678";

        const result = await fetchCep(cep);

        expect(result.cep).toBe("12345678");
        expect(result.logradouro).toBe("Avenida Paulista");
        expect(result.bairro).toBe("Centro");
        expect(result.localidade).toBe("São Paulo");
        expect(result.uf).toBe("SP");
    });
});
Enter fullscreen mode Exit fullscreen mode

At first, we are setting up the mock for the axios library and its get function.

const mockAxiosGet = jest.fn();
jest.mock("axios", () => ({
      get: mockAxiosGet,
}));
Enter fullscreen mode Exit fullscreen mode

Right after that, we import our fetchCep function:

const { fetchCep } = require("./axiosMockExample");
Enter fullscreen mode Exit fullscreen mode

The first step in the "should return cep information" test is to set up the return value of the get method of the axios library:

mockAxiosGet.mockResolvedValueOnce({
    data: {
        cep: "12345678",
        logradouro: "Avenida Paulista",
        bairro: "Centro",
        localidade: "São Paulo",
        uf: "SP",
    },
});
Enter fullscreen mode Exit fullscreen mode

Once the get method's return value is configured, we call our fetchCep function and validate whether we received the expected result:

const cep = "12345678";

const result = await fetchCep(cep);

expect(result.cep).toBe("12345678");
expect(result.logradouro).toBe("Avenida Paulista");
expect(result.bairro).toBe("Centro");
expect(result.localidade).toBe("São Paulo");
expect(result.uf).toBe("SP");
Enter fullscreen mode Exit fullscreen mode

Now, we simply run our test using Jest:

Success test results

Great! Our test passed as expected! 🥳

However, what if, for some reason, we need to change the HTTP request library we are currently using, like axios? What would happen to our implemented test? Let's find out?

Let's assume that the axios library has been replaced with the native Fetch API of Node.js:

async function fetchCep(cep) {
    const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);

    return response.json();
}

module.exports = {
    fetchCep,
};
Enter fullscreen mode Exit fullscreen mode

Now, let's run our unit test:

Fail test results

As we can see, our test has now failed. The reason for the failure is related to the fact that we had configured a mock that was tightly coupled to the library we were using previously, axios.

To resolve this dependency, we will use the Mock Service Worker (MSW) library. This library is a tool that allows you to simulate and intercept network requests in web applications and Node.js during testing. It provides a convenient way to create mocks for the responses of network calls, enabling developers to control the behavior of HTTP requests made by the application in unit tests without actually making requests to the real network. This is especially useful for isolating tests and ensuring that the application functions correctly even when the API or external services are unavailable, while also simplifying the simulation of different network response scenarios for testing purposes.

Now, let's set up the service worker in our test file. To do this, we need to install the MSW package in our development dependencies using npm or yarn:

npm install msw --save-dev
Enter fullscreen mode Exit fullscreen mode

In the test file, we'll import the rest object from the msw package and the setupServer function from the msw/node package:

const { setupServer } = require("msw/node");
const { rest } = require("msw");
Enter fullscreen mode Exit fullscreen mode

Before the declaration of our test suite using describe, let's set up our server, which will be responsible for intercepting all calls made by any HTTP request library:

const server = setupServer(
    rest.get("https://viacep.com.br/ws/:cep/json/", (req, res, ctx) => {
        return res(
            ctx.status(200),
            ctx.json({
                cep: "12345678",
                logradouro: "Avenida Paulista",
                bairro: "Centro",
                localidade: "São Paulo",
                uf: "SP",
            })
        );
    })
);
Enter fullscreen mode Exit fullscreen mode

As we can see, we are configuring the interceptor's behavior for GET requests to the endpoint https://viacep.com.br/ws/:cep/json/. Note that in :cep, we are abstracting any ZIP code provided during the HTTP request. The configuration set will return a status code of 200 and the object containing ZIP code information in the response body.

For more information on configuring interceptors, I recommend consulting the official MSW documentation.

We're almost there! We need the following configurations:

  • Before all tests in the suite, initialize our server:
beforeAll(() => server.listen());
Enter fullscreen mode Exit fullscreen mode
  • After each test in the suite, restore the original configuration of our server, as defined above. This restoration ensures that if we add new handlers or overwrite existing ones within the tests, the server's configuration is the original one before each test is executed within our suite:
afterEach(() => server.resetHandlers());
Enter fullscreen mode Exit fullscreen mode
  • After all tests in the suite, shut down our server:
afterAll(() => server.close());
Enter fullscreen mode Exit fullscreen mode

The last two points to change in our test file are the removal of the mock for the axios library and the configuration of the return of its get method:

// REMOVER
const mockAxiosGet = jest.fn();
jest.mock("axios", () => ({
    get: mockAxiosGet,
}));

...

// REMOVER
mockAxiosGet.mockResolvedValueOnce({
    data: {
        cep: "12345678",
        logradouro: "Avenida Paulista",
        bairro: "Centro",
        localidade: "São Paulo",
        uf: "SP",
    },
});
Enter fullscreen mode Exit fullscreen mode

Finally, our test implementation will look like this:

const { setupServer } = require("msw/node");
const { rest } = require("msw");
const { fetchCep } = require("./axiosMockExample");

const server = setupServer(
    rest.get("https://viacep.com.br/ws/:cep/json/", (req, res, ctx) => {
        return res(
            ctx.status(200),
            ctx.json({
                cep: "12345678",
                logradouro: "Avenida Paulista",
                bairro: "Centro",
                localidade: "São Paulo",
                uf: "SP",
            })
        );
    })
);

describe("fetchCep", () => {
    beforeAll(() => server.listen());

    afterEach(() => server.resetHandlers());

    afterAll(() => server.close());

    it("should return cep information", async () => {
        const cep = "12345678";

        const result = await fetchCep(cep);

        expect(result.cep).toBe("12345678");
        expect(result.logradouro).toBe("Avenida Paulista");
        expect(result.bairro).toBe("Centro");
        expect(result.localidade).toBe("São Paulo");
        expect(result.uf).toBe("SP");
    });
});
Enter fullscreen mode Exit fullscreen mode

Now, our test is passing successfully:

Success test results

In this example, we focused on an HTTP GET request. However, the MSW library allows us to configure interceptors for all HTTP verbs.

I strongly recommend studying the official documentation of the library for implementing tests in your projects and applications.

💖 💪 🙅 🚩
giraldidev
Leonardo Giraldi Moreno Giuranno

Posted on October 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related