Library-Agnostic Mocks for HTTP Request Libraries in Node.js
Leonardo Giraldi Moreno Giuranno
Posted on October 23, 2023
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,
};
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");
});
});
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,
}));
Right after that, we import our fetchCep
function:
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:
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:
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:
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,
};
Now, let's run our unit test:
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
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");
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",
})
);
})
);
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());
- 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());
- After all tests in the suite, shut down our server:
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:
// 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:
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:
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.
Posted on October 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.