Library-Agnostic Mocks for HTTP Request Libraries in Node.js
Tags: node, testing, javascript
Published: 2023-10-23 11:30:00

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:

```jsx
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,
};
```

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](https://viacep.com.br/). 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:

```jsx
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");
    });
});
```

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

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

Right after that, we import our `fetchCep` function:

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

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:

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

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

```jsx
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");
```

Now, we simply run our test using Jest:

![Success test results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmfogzt2gp3azd9dij3l.png)

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](https://www.notion.so/Mocks-agn-sticos-para-bibliotecas-de-requisi-es-HTTP-no-Node-js-ae51be87c5a840c3ac8d913134de2c52?pvs=21):

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

    return response.json();
}

module.exports = {
    fetchCep,
};
```

Now, let's run our unit test:

![Fail test results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fnmwsfu79hyrw5h9klbh.png)

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:

```bash
npm install msw --save-dev
```

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

```jsx
const { setupServer } = require("msw/node");
const { rest } = require("msw");
```

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:

```jsx
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",
            })
        );
    })
);
```

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](https://mswjs.io/docs/).

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

- Before all tests in the suite, initialize our server:

```jsx
beforeAll(() => server.listen());
```

- 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:

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

- After all tests in the suite, shut down our server:

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

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:

```jsx
// 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",
    },
});
```

Finally, our test implementation will look like this:

```jsx
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");
    });
});
```

Now, our test is passing successfully:

![Success test results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vejjow7efjpjsnt4mex0.png)

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.