React unit testing using Vitest, RTL and MSW
Mohamed Aymen Ourabi
Posted on February 15, 2024
Introduction
Hello everyone, i'm finally back after almost 5 years since my last post in this community, i would like to apologize to everyone that i couldn't answer their questions, i completely lost access to this account a few years ago and it was a hell of a road to get it back.
Today i would like to share with you a quick tutorial about how to use MSW (Mock service worker) inside React.js application for both testing and development
Getting started
What is MSW ?
MSW or mock service worker is an api mocking library used mostly to mock api calls, it is mainly using browser Service workers to set up their system and to be able to intercept requests at network levels
Why using MSW ?
Well there are a couple of reasons why we use MSW.
- For development it is pretty usefull for frontend developers to prevent them being blocked during their tasks in case there are some issues in backend side. We all know that these things occurs sometimes when the server is down or when there is something that should be fixed and we find that we depend on the data that should be returned to continue progressing. However using MSW we can create our mocks exactly with the same structure that we have in backend side and this will completly be on a separated box from what http client we are using either fetch or axios our bussiness logic will remain the same
- For testing we all know that most of our projects rely on http requests and when creating unit test we find that we are mostly forced to mock these requests inside our tests
import { http } from "./api/myService";
jest.mock("./api/myService");
describe('Fetch data , () => {
it('should render active user when isActive us true', async () => {
http.mockResolvedValue(
{
id: '1',
name: 'Mohamed Aymen',
isActive: STATUS.Active,
},
)
render(<MyComponent />)
expect(screen.getByTestId('isActive')).toBeInTheDocument()
})
}
In this example we can see that we mocked the returned value of an http request in order to test this behavior.
But what happen when we need this mocked value in other test cases ?
we will have to mock this request everytime we need to use it
and what if we need something reusable accros all of our tests?
*This is where the power of MSW comes in hand :)) *
With MSW the mocks will be created on a separted box and will run in parallel with our application logic as well as tests so we don't need to mock the returned values everytime as will get automatically the responses from our mocks in MSW
Setup MSW
for this tutorial we will need to create a React application first so under you project directory open terminal then run the following command
npm create vite@latest
we will use TypeScript template because i personally think TypeScript is just AWESOME :))
Install MSW and Axios
Under /yourdirectory/my-msw-app run this command
npm install msw --save-dev
and
npm install axios --save
Now that MSW is installed let's create our React component.
To keep this tutorial quick and simple we will try to create a simple component that allows us to send an http requests that return a user with a parameters status with that we can check if this user is Active or not
first create the componnet Profile/Profile.tsx
under /src
import React, { useEffect, useState } from 'react'
import { STATUS } from './consts'
import axios, { AxiosError, AxiosResponse } from 'axios';
const Profile = () => {
axios.defaults.baseURL = 'http://localhost:8000';
const [status, setStatus] = useState<STATUS>(STATUS.OFFLINE);
useEffect(() => {
axios.get('api/getStatus').then((res: AxiosResponse<{isActive: STATUS }>) => {
setStatus(res.data.isActive)
}).catch((err: AxiosError) => {
console.error({ err })
})
}, [])
return (
<div>
{
status === STATUS.ACTIVE ?
<div data-testid="user-active">
this user is active
</div>
:
<div data-testid="user-offline">
this user is offline
</div>
}
</div>
)
}
export default Profile
then create a consts.ts
file to declare our Enum
export enum STATUS {
ACTIVE="ACTIVE",
OFFLINE = "OFFLINE"
}
initialize our MSW
Under src create a folder called msw
then create inside two files worker.ts
and handlers.ts
//Worker.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const serviceWorker = setupWorker(...handlers);
In this file we initlize our workers and pass the handlers
//handlers.ts
import { http, HttpResponse } from "msw";
import { STATUS } from "../UserProfile/consts";
export const handlers = [
http.get("http://localhost:8000/api/getStatus", () => {
return HttpResponse.json({
isActive: STATUS.ACTIVE,
})
}),
];
In this file we declare our handlers and setup our mocks
as you can see the endpoint /api/getStatus
is basically the same used in axios so no need to override or rewrite anything as MSW will automatically intercept this request and return our mock
final step is to call the worker inside our index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
async function enableMocking() {
if (!Boolean(import.meta.env.VITE_ENABLE_MSW)) {
return
}
const { worker } = await import('./msw/workers')
// `worker.start()` returns a Promise that resolves
// once the Service Worker is up and ready to intercept requests.
return worker.start()
}
enableMocking().then(() => {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
});
As you can see the msw will be triggered in one condition if the env variable VITE_ENABLE_MSW
is set to true
for this we need to create a .env.local
file and also update scripts in package.json
//.env.local
VITE_ENABLE_MSW=false
"scripts": {
"dev": "vite",
"dev-msw": "set VITE_ENABLE_MSW=true && vite",
"msw-init": "npx msw init ./public --save",
}
NOTE:
if we want msw to be used in our development env we can just run our project using npm run msw-dev
but before that we need to execute npm run msw-init
in order to regsiter our mockServiceWorker.js
inside /public
so that the file will be accessible via http://localhost:3000/mockServiceWorker.js
.
After running our command we should be able to access our React app at http://localhost:3000/
then if we want to make sure that msw is working fine we can check our chrome devTools under application=>service worker
now we are ready to use it !!
*Run the project *
Once our project running we can check our http://localhost:3000/
to check our app
if eveything was fine we should see this "User is active" text is displayed !!
if we try to check our network in chrome devTools for example we should see that the request has been successfully intercepted by MSW and we get our mocked result returned to us
As we can see we can use MSW this way during development and specially when our backend is down or blocked we can at least mock some endpoints and continue progressing
MSW and React Testing
In order to setup MSW for unit test we should proceed as follow.
1- install React testing library and Vitest
npm install vitest jsdom @testing-library/react @testing-library/jest-dom --save-dev
2- go to vite.config.ts and add the following config
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.ts',
},
})
3- update our msw/worker.ts
For test purposes we need to change our previous import from
import { setupWorker } from 'msw/browser'
to
import { setupServer } from 'msw/node'
thus the file will look like this
//Worker.ts
import { setupServer} from "msw/node";
import { handlers } from "./handlers";
export const serviceWorker = setupServer(...handlers);
4- create a file called src/setupTests.ts
and add the following config
import { expect, afterEach, beforeAll, afterAll } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from "@testing-library/jest-dom/matchers";
import { worker } from './msw/workers';
expect.extend(matchers);
afterEach(() => {
cleanup();
});
// Start worker before all tests
beforeAll(() => { worker.listen() })
// Close worker after all tests
afterAll(() => {worker.close()})
// Reset handlers after each test `important for test isolation`
afterEach(() => {worker.resetHandlers()})
Before each test worker will start in order to setup our handlers then after each test it will reset the handlers to keep our mocks consistent then once all tests are finished the worker will be stoped
5- update package.json
"scripts": {
"test:unit": "vitest --root src/"
}
6- Now create a unit test for our React component
First let make sure that our component is rendering without any issue
import { expect, test, describe } from 'vitest'
import { render } from '@testing-library/react';
import Profile from './profile';
describe('profile Component', () => {
test('should render without crashing', () => {
const { getByTestId } = render(<Profile />);
const root = getByTestId('root');
expect(root).toBeInTheDocument()
})
})
then run :
npm run test:unit
Now let's try to check if msw is working inside tests, we will try to test if the component is returning the text "this user is active" which mean that the http requests to the msw are returning 200 and that we get what we want.
For that we will use the testid's to check if the component is rendering
test('should render user is active when server retun Status:Active', async() => {
render(<Profile />);
await waitFor(() => {
const section1 = screen.queryByTestId('user-active');
const section2 = screen.queryByTestId('user-offline');
expect(section1).toBeInTheDocument();
expect(section2).toBeNull();
});
})
And Bingo !! our tests are working
the full content of the test file:
import { expect, test, describe } from 'vitest'
import { render , screen, waitFor } from '@testing-library/react';
import Profile from './profile';
describe('profile Component', () => {
test('should render without crashing', () => {
const { getByTestId } = render(<Profile />);
const root = getByTestId('root');
expect(root).toBeInTheDocument()
})
test('should render user is active when server retun Status:Active', async() => {
render(<Profile />);
await waitFor(() => {
const section1 = screen.queryByTestId('user-active');
const section2 = screen.queryByTestId('user-offline');
expect(section1).toBeInTheDocument();
expect(section2).toBeNull();
});
<span class="p">})</span>
})
Conclusion
MSW is a powerfull tool to use in our projects for development as well as testing it allows us to have a central setup mocks for our tests and removes bunch of noizy mocks from our test files also it keeps them consistent.
Any questions or issues please let us know in your comments
Cheers!!
Posted on February 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.