DEV Community

Cover image for React unit testing using Vitest, RTL and MSW
Mohamed Aymen Ourabi
Mohamed Aymen Ourabi

Posted on • Updated on

React unit testing using Vitest, RTL and MSW

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()
    })
}
Enter fullscreen mode Exit fullscreen mode

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 :))

vite typescript

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
Enter fullscreen mode Exit fullscreen mode

then create a consts.ts file to declare our Enum

export enum STATUS {
    ACTIVE="ACTIVE",
    OFFLINE = "OFFLINE"
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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,
        })
    }),
];
Enter fullscreen mode Exit fullscreen mode

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>
  );
});

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  "scripts": {
    "dev": "vite",
    "dev-msw": "set VITE_ENABLE_MSW=true && vite",
    "msw-init": "npx msw init ./public --save",
}
Enter fullscreen mode Exit fullscreen mode

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.jsinside /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

Image d

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

app running

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

result status 200

result json success

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
Enter fullscreen mode Exit fullscreen mode

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',
  },
})

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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()})
Enter fullscreen mode Exit fullscreen mode

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/"
}
Enter fullscreen mode Exit fullscreen mode

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()
    })

})

Enter fullscreen mode Exit fullscreen mode

then run :
npm run test:unit

Testing component
and tests are working fine!!

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

testid on components

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();
          });

    })
Enter fullscreen mode Exit fullscreen mode

And Bingo !! our tests are working

tests 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();
          });

    })
})

Enter fullscreen mode Exit fullscreen mode

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!!

Top comments (0)