Kent C. Dodds recently wrote a very interesting post calling an end to mocking window.fetch
when testing React applications:
He was right.
I just recently had to migrate a React project from a fetch based API client implementation to an axios based one where the tests heavily relied on mocking global.fetch
. It very quickly became apparent why this is not good practice.
I ended up having to write my own test utility that would mock both fetch and the new API client. It never looks good when you have to change tests to prove your code didn't change anything for the user.
As a better alternative, Kent suggests using a Mock Service Worker. More specifically the msw module to essentially run a mock backend as a service worker that intercepts all outgoing API requests to handle them.
Setting up msw
Setting up a mock backend with msw for your React tests turns out to be a fairly easy process. To get a deeper picture you should check out Kent's original post, but here's all you really need to do in your test code to mock a REST endpoint:
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/pets', (req, res, ctx) => {
const pets = [{ id: 1, name: 'Garfield', type: 'cat' }];
return res(ctx.json({ pets }));
}),
);
beforeAll(() => server.listen());
afterAll(() => server.close());
One of the reasons this is extremely cool is because it avoids the pain of having to start up a real local mock backend, such as an express server that needs to be bound to a specific port on the host running the test.
This helps keep your tests fast and simple to run, as they should be.
Even better with OpenAPI
As someone who works a lot with API backends that (hopefully!) provide Swagger/OpenAPI definitions, I had already been mocking my backends in React tests using OpenAPI mocks with openapi-backend. When I learned about msw
, I was thrilled!
It turns out msw
together with openapi-backend
is the perfect combination for mocking REST apis.
To provide a full mock for an API, all I need is to create a mock backend with openapi-backend using the API definition and tell msw to use it:
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import OpenAPIBackend from 'openapi-backend';
import definition from './path/to/definition.json';
// create our mock backend with openapi-backend
const api = new OpenAPIBackend({ definition });
api.register('notFound', (c, res, ctx) => res(ctx.status(404)));
api.register('notImplemented', async (c, res, ctx) => {
const { status, mock } = api.mockResponseForOperation(c.operation.operationId);
ctx.status(status);
return res(ctx.json(mock));
});
// tell msw to intercept all requests to api/* with our mock
const server = setupServer(
rest.all('/api/*', async (req, res, ctx) => api.handleRequest(
{
path: req.url.pathname,
query: req.url.search,
method: req.method,
body: req.bodyUsed ? await req.json() : null,
headers: { ...req.headers.raw },
},
res,
ctx,
)),
);
beforeAll(() => server.listen());
afterAll(() => server.close());
Now instead of having to write your own mock handlers for each operation, they're generated from the response schemas and examples defined in the OpenAPI document.
What's more: any time the API definition changes, all your mocks will be automatically updated giving you further confidence your app is compatible with the new API version.
Enabling Request Validation
When testing, it's often very useful to make sure your application is actually sending the correct requests to the API.
Working with OpenAPI definitions has the benefit that API operations are well defined and requests can be automatically validated using JSON schema.
To enable request validation during tests, you can simply register the validationFail handler for openapi-backend:
api.register('validationFail', (c, res, ctx) => res(
ctx.status(400),
ctx.json({ error: c.validation.errors }),
));
When running tests, a malformed call to an API endpoint will now result in a 400 Bad Request error from the mock backend, alongside a useful error message telling you what's wrong with the request.
Custom Handlers
In some tests it might make sense to provide a different mock than the default one as provided by openapi-backend.
Registering your own mock for an API operation in a test is as simple as calling api.register()
with the operationId and a mock handler:
it('should call getPets operation', () => {
// given
const mockResponse = [{ id: 2, name: 'Odie' }];
const mockHandler = jest.fn((c, res, ctx) => res(ctx.json(mockResponse)));
api.register('getPets', mockHandler);
// when
// render(<MyComponent />)...
// then
expect(mockHandler).toBeCalled();
});
Conclusion
Finding out about msw was a major game changer for my React testing. This in combination with the auto-mocking capabilities of openapi-backend makes mocking APIs in React tests a breeze.
Thank you, Kent, and the team behind mswjs/msw! π
Top comments (5)
Hey Viljami,
nice article π,
I have been using Mocklets for my app development, and its been great, specially there dynamic response feature, which allows you to set multiple responses for single api to verify various test cases.
Nice! Mocklets seems super cool. I'll definitely have to look into it more. Always love seeing companies popping up innovating in this space!
I absolutely love the use case Viljami illustrates in this article! Super excited to see how MSW integrates with this setup. Keep up the great work!
Very nice article but leaving me with some questions:
Check out Prism also: it offers OpenAPI 2/3 and Postman Collection mocks and validation: github.com/stoplightio/prism