When we talk about testing our applications, one of the main issues we need to keep in mind is how we should deal with the requests that our application makes to the server, as virtually every application interacts with requests in some way.
To many, this process may sound familiar and even simple. If you already have experience with this part, perhaps the first thing that comes to mind is to simply create a mock with jest
, something like this (axios):
// __mocks__/axios.js
export default {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} })),
put: jest.fn(() => Promise.resolve({ data: {} })),
// ...
}
// how it can be implemented in our tests
import mockAxios from "axios"
test("a simple test", () => {
...
mockAxios.get.mockImplementationOnce(() => Promise.resolve({ data: {} }))
...
})
However, it may not be the best solution or at least the most robust one, what do I mean by that?
When we mock some library as axios, we are inferring that the request was made correctly, that is, that if our request required sending certain headers, for example, these were sent correctly when perhaps they were not and, since our test is probably not focused on making sure those headers were sent, we run the risk of letting a mistake go by.
On the other hand, this is not the only complexity that can be presented to us with this approach, as many know, another of the needs that the form outlined above requires, is to repeat code so that each test can have the implementation and response that we need. This can result in very large files that are difficult to maintain.
msw stands for Mock Service Worker
, a tool that takes care of intercepting all requests made at your network level. Let’s explain a little more in detail what this is all about.
The basic idea of msw is to create a fake server that intercepts all requests and handles them like a real server. This implies that you can implement this tool as a set of “databases” either from json files to “seed” the data, or “constructors” like fakerJS and then create drivers (similar to the express API) and interact with that simulated database. This allows us to test our application under conditions very similar to those in production, either for development, testing, or debugging. In the case of testing, which is what we will focus on today, it allows for quick and easy tests to be written.
Once all this is explained, we can move on to writing code.
What we will do this time is a very simple demo of how to work with msw
, we will take as a base a very basic ToDos application made with create-react-app
and exploring the tests with jest
and react-testing-library
.
Let’s get started.
msw provides us with an npm package (also available with yarn) to use in our project.
$ npm i msw -D
$ yarn add msw — dev
2. Creating our first controller
As we commented previously, msw works with a syntax very similar to what we know from express
making it very fast and easy to build the drivers that will take care of emulating our backend.
// src/mocks/server.js
import { rest } from "msw";
import { setupServer } from "msw/node";
const handlers = [
rest.get("http://localhost:4000/todos", (req, res, ctx) => {
return res(ctx.status(200), ctx.json([{ id: 1, body: "first todo" }]));
}),
];
// This configures a request mocking server with the given request handlers.
const server = setupServer(...handlers);
export { server, rest };
As we can see in the code, we created a file called server.js
inside a folder called mocks
, in that file we made the import of an object called rest
from msw, since it is the architecture that we will follow, however, it is important to highlight that msw also allows you to emulate the flow if you work with GraphQL.
Besides the import, we created a constant called handlers that will contain all the controllers we want to include in our server. The server object contains methods corresponding to the http verbs like get
and post
, the syntax is very simple, the method only needs to be passed 2 parameters, the url
and a “resolver” which will receive req, res and ctx
which we will explain a little later:
req
, contains information about the petition.res
, a functional utility that takes care of sending the response.ctx
, a group of functions that allow us to configure status codes, headers, the body of the response, among other things.In order for msw to intercept the request, it is important that we place the url
correctly, there are 3 ways in which msw can evaluate the matches.
e.g. http://localhost:4000/todos
e.g. /todos
You can find more information about it here.
3. Jest Configuration
For this step, we are going to take advantage of creating an application with the create-react-app
since it gives us a completely configured environment with the tools we are going to need not only to be able to develop our project but also to be able to test it correctly.
Inside our project, we’ll find a file called setupTests.js
setupTests.js inside the src
folder, it’s here where we’ll be able to add extra configuration that will be useful in our tests, for example, by default it comes imported the module “@testing-library/jest-dom/extend-expect” that allows us to use affirmations like toBeInTheDocument
or toHaveAttribute
, it’s inside this file where we’ll add the following:
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect";
/**************
* MSW config code
***************/
import { server } from "./mocks/server";
// Establish API mocking before all tests.
beforeAll(() => server.listen());
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());
As we can see, the required configuration is very simple, just import the server we have just configured and added three global conditions for our tests:
beforeAll
, raise the server so that it is waiting for the requests before my tests start running.afterEach
, after each test it will reset the drivers to the initial values, and during the tests we can add new drivers that were not defined previously, a topic that we will cover a little later, this to avoid that these new drivers affect other tests.afterAll
, after all the tests are finished, we finish the “server”.By this point we have covered everything we need to configure msw
in our project, but how do we implement it?
4. Implementation
Actually the implementation is just as easy as the configuration we have done so far. Let’s consider that we have a simple component that just requests our ToDos from the server and draws them in the browser as follows:
import React, { useEffect, useState } from "react";
// considering we have the service created
import { getTodos } from "./services/todos.service";
function App() {
const [todos, setTodos] = useState([]);
const [error, setError] = useState();
useEffect(() => {
getTodos()
.then((res) => {
const { data: todos } = res;
setTodos(todos);
})
.catch((err) => {
setError(err.response.data.message);
});
}, []);
return (
<div className="App">
{error && <h1>{error}</h1>}
<ul>
{todos.map((todo, i) => (
<li key={i}>{todo.body}</li>
))}
</ul>
</div>
);
}
export default App;
#mockservice #jest #react #testing #msw