Configuring Mock Service Worker (MSW)

Configuring Mock Service Worker (MSW)

ยท

11 min read

Configuring Mock Service Worker (MSW)

Image for post

Photo by Christopher Gower on Unsplash

Are you building an app where you don't have your backend implementation yet? Are you tired of maintaining a dedicated mock server? Do you find that mocking fetch or axios causes more problems in your tests than it fixes? Then you should probably consider starting using Mock Service Worker (MSW).

Recently, I've configured MSW in a couple of projects. Despite MSW being simple to configure there were some scenarios where I had issues. This blog post will do a small introduction to MSW, followed by the base steps while configuring it, and end with some issues I had.

What is MSW?

Mock Service Worker is an API mocking library that uses Service Worker API to intercept actual requests.

In a short description, MSW leverages service workers to intercept requests on the network level and return mocked data for that specific request. Thanks to MSW, by having a defined API contract you can return mocked data even before that endpoint exists. Also, by leveraging the same mocked data in your tests, you no longer need to mock axios or fetch, just let MSW do its work.

Note: Service workers only work in a browser environment. In a node environment (e.g for tests), MSW leverages a request interceptor library for node and allows you to reuse the same mock definitions from the browser environment.

Adding MSW to your app

The first thing you should do is install MSW as a dev dependency:

yarn install msw --dev

Afterward, so that you can run MSW in the browser, you have to add the mockServiceWorker.js file. This can be done, by doing running the following command targeting the public folder:

npx msw init public/

Request handler and Response resolver

A request handler allows you to specify the method, path, and response when handling a REST API request.

A response resolver is a function you pass to the request handler that allows you to specify the mocked response when intercepting a request.

Before configuring anything, I usually create a handlers.js file with some request handlers. Here's an example:

import { rest } from 'msw'

export const handlers = [
  rest.get('*/superhero', (req, res, ctx) =>
    res(
      ctx.status(200),
      ctx.json([
        { superheroName: 'Batman' },
        { superheroName: 'Superman' },
        { superheroName: 'Flash' },
      ]),
    ),
  ),
]

In the handlers array above, I'm providing it a request handler for a GET request to the /superhero endpoint. Afterward, I'm passing it a response resolver that will guarantee that a request to that endpoint will return a 200 status code and a specific JSON object. Now that we have our handlers, we can start configuring MSW.

Configuring MSW for the browser

The first thing we need is to create an instance of our worker. This can be done by creating a mswWorker.js file and inside of it do the following:

import { setupWorker } from 'msw'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

When setting up your worker you need to pass it your handlers. As you can see we export worker so that we can import it on our index.js and start it up. On your index.js file do the following:

import { worker } from './mswWorker'

worker.start()

Afterward, you just need to start your app and you are set to go.

Configuring MSW for your tests

For running MSW in your tests, the scenario is identical to the above one. The only difference is that instead of using setupWorker, what we do is use setupServer. The following snippet is added to a mswServer.js file.

import { setupServer } from 'msw/node'
import { handlers, defaultHandlers } from './handlers'

export const server = setupServer(...handlers, ...defaultHandlers)

As you can see, I've passed extra handlers to my setupServer that I didn't do one the one above. The reason for that is that in my test files I want to have extra handlers to intercept all requests that I'm not targeting on my normal handlers. To do that, I created a defaultHandlers. What I include in it is the following:

export const defaultHandlers = [
  rest.get('*', (req, res, ctx) => res(ctx.status(200), ctx.json({}))),
  rest.post('*', (req, res, ctx) => res(ctx.status(200), ctx.json({}))),
  rest.patch('*', (req, res, ctx) => res(ctx.status(200), ctx.json({}))),
  rest.put('*', (req, res, ctx) => res(ctx.status(200), ctx.json({}))),
  rest.delete('*', (req, res, ctx) => res(ctx.status(200), ctx.json({}))),
]

Now that we have our server instance, we need to start it before each test scenario. Also, we need to guarantee that we reset our handlers (just in case we added some handlers during a specific test scenario) and that after each test, we shut down our server. To do so, in our setupTests.js file, add the following:

import { server } from './mswServer'

beforeAll(() => server.listen())

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

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

After this, MSW should be running in your tests.

Testing network error scenario

For testing network errors on my application, I usually create a networkErrorHandlers in my handlers.js file.

export const networkErrorHandlers = [
  rest.get('*', (req, res, ctx) => res.networkError('Boom there was error')),
  rest.post('*', (req, res, ctx) => res.networkError('Boom there was error')),
  rest.patch('*', (req, res, ctx) => res.networkError('Boom there was error')),
  rest.put('*', (req, res, ctx) => res.networkError('Boom there was error')),
  rest.delete('*', (req, res, ctx) => res.networkError('Boom there was error')),
]

Then in my test file, I import the networkErrorHandlers along with our server instance and do the following:

test('should show error message on error', async () => {
  server.use(...networkErrorHandlers)
  render(<App />)
  const errorMessage = await screen.findByText(/There was an error/i)
  expect(errorMessage).toBeInTheDocument()
})

In this test example, by using server.use(...networkErrorHandlers) I'm telling my server instance to use those given handlers before any other handler passed before. This guarantees that the networkError will always occur.

Adding handlers during a test runtime

Sometimes, in a specific test you want to override some previously created handlers to a given endpoint. This can be done by leveraging the server instance and passing it a new handler.

test('should show error message on error', async () => {
  server.use(
    rest.get('*', (req, res, ctx) =>
      res(ctx.status(400), ctx.json({ errorMessage: 'hello' })),
    ),
  )
  render(<App />)
  const errorMessage = await screen.findByText(/There was an error/i)
  expect(errorMessage).toBeInTheDocument()
})

On the test above, by using the server.use() and passing it a new request handler and a response resolver, we are telling MSW to prioritize that handler before the previously configured ones. By doing this you can add new handlers that are only specific to your test.

On both of the last topics we leveraged the server.use() to add new handlers. As you remember, on our setupTests we added the following afterEach(() => server.resetHandlers()). This condition guarantees that after each test, we remove the added handlers and avoid having tests leaking into each other.

Final considerations

MSW changed the way I've been writing tests for the better. By creating handlers, the amount of boilerplate code I've removed has enormous, and thanks to it, my tests have become easier to understand. Before wrapping this blog post, here are some issues I've run while setting up MSW.

  • If you are using Webpack instead of create-react-app do not forget to add your public folder to the devServer contentBase property.
  • If you are running your application inside of an iframe, make sure to enable chrome://flags/#unsafely-treat-insecure-ori.. and provide it with the url where the application is loaded from.

That wraps this post. I hope you all enjoyed it! Stay tuned for the next one!

Originally published at danieljcafonso.com.