Testing with MSW
Tags: testing, javascript, react
Published: 2023-02-11 04:53:50

## Problem
When mocking api, we usually got the problem, which is:

> Duplication of mocking same api

&nbsp;

### description
When we mock api, there are some problems, such as:

- **Duplication of mocking same apis in different Components:** 
When testing different components, 
assuming the same api is used again and again, 
we will need to re-mock the api,
(Although you can put the mock api under `/__mock__` to avoid duplication, 
but there will be Typescript type problems at present)

&nbsp;

- **Duplication of mocking same apis in Hook & Component**:
If there is a custom hook that will open the api, 
when we are testing the custom hook, 
we have already written a mock api, 
when we are testing the component that uses the custom hook, we must rewrite the mock api when testing the component, resulting in inconvenience for maintenance

&nbsp;

(Hook's code)

```jsx
// Hook's production code

const useUserLocations = () => {
  const [userLocations, setUserLocations] = useState();

  const fetchUserLocations = async () => {
    const users = await apiGetUsers();
    const locations = users. map((user) => user. location);
    return locations;
  };

  useEffect(() => {
    fetchUserLocations()
      .then((locations) => {
        setUserLocations(locations);
      })
  }, []);

  return userLocations;
};
```

```jsx
// Hook's testing code

describe('useFetchUserLocations', () => {
  test('by default, should return an array containing users 
   locations', () => {
     // Arrange
     apiGetUser.mockResolvedValue([
       { name: 'Alen', location: 'America' },
       { name: 'Benson', location: 'Taiwan' },
       { name: 'Camillie', location: 'America' },
     ]);

    // Act
    const { result } = renderHook(useFetchUserLocation);

    // Assert
    expect(result.current).toEqual(['American', 'Taiwan', 'America']);
  });
});
```

&nbsp;

(Component's code)

```jsx
// Component's production code

import useUserLocations from '@/hooks/useUserLocations';

const UserStatic = () => {
  const userLocations = useUserLocations(); // using the hook above

  return (...); // pretended this render a pie chart with label
};
```

```jsx
// Component's testing code

describe('UserStatic', () => {
  test('when users exist and have locations, should show location label', () => {
    // Arrange
    apiGetUser.mockResolvedValue([
      { name: 'Alen', location: 'America' },
      { name: 'Benson', location: 'Taiwan' },
      { name: 'Camillie', location: 'French' },
    ]); // mock the same value again !!

    // Act
    const { getByTestId } = render(<UserStatic />);
    const labelAmerica = getByTestId('label-America');

    // Assert
    expect(labelAmerica).toBeVisible();
  });
});
```

While I was researching how to test swr,
I suddenly found an article ( [Stop mocking fetch by Kent C. Dodds](https://kentcdodds.com/blog/stop-mocking-fetch) ) written about how to solve this problem ,
instead of writing the mock API again and again in the test file,
we can actually fake the whole api service!!!

We can make our unit test really call the api,
but the responses provided by the fake service, 
and these fake services will centrally manage these APIs,
which can reduce the repeat api mocking,
it is also convenient for us to centrally manage apis.



&nbsp;&nbsp;



## Introduction to MSW

The full name of MSW is Mock Service Worker,
which allows us to forge service workers,
let our testing code be able to call api as the same way production code does, 
but it will be processed by msw,
and return our own custom mocking responses.

The setting method is as follows:

```jsx
import { rest } from 'msw' // msw supports graphql too!
import * as users from './users'

const handlers = [
   rest.get('/users', async (req, res, ctx) => {
     const users = [
       { name: 'Alen', location: 'America' },
       { name: 'Benson', location: 'Taiwan' },
       { name: 'Camillie', location: 'French' },
     ];
     return res(ctx.json(users));
   }),
   rest.post('/users', async (req, res, ctx) => {
     if (req.name && req.email && req.location) {
       return res(
         ctx. staus(200)
         ctx.json({ success: true })
       );
     }
   }),
];

export { handlers };
```

```jsx
//test/server.js
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { handlers } from './server-handlers'

const server = setupServer(...handlers)
export { server, rest };
```

```jsx
//test/setup-env.js
// add this to your setupFilesAfterEnv config in jest so it's imported for every test file
import {server} from './server.js'

beforeAll(() => server. listen())
// if you need to add a handler after calling setupServer for some specific test
// this will remove that handler for the rest of them
// (which is important for test isolation):
afterEach(() => server. resetHandlers())
afterAll(() => server. close())
```

&nbsp;&nbsp;


## Rethink the purpose of our fake api

When we write tests,
sometimes we want to call the api with the correct parameters

```jsx
const useUser = (userUuid) => {
  const [userLocations, setUserLocations] = useState();

  const fetchUser = async () => {
    const user = await apiGetUser(userUuid);
    return user;
  };

  useEffect(() => {
    fetchUserLocations()
      .then((locations) => {
        setUserLocations(locations);
    })
  }, []);

  return userLocations;
};
```

```jsx
const apiGetUser = jest.fn();

test('when passed user uuid, should call apiGetUser with the same user uuid', () => {
  // Act
  const { result } = render(() => useUser('mockUserUuid'));

  // Assert
    expect(apiGetUser).toHaveBeenCalledWith('mockUserUuid');
});
```

&nbsp;&nbsp;

But when using mock service worker,
we don't need to mock api function,
so we cannot monitor the parameters entered when the api function is called,

How do we test it at this time?

In fact, it is the same as when the real backend is doing it!
Different input values return different input results!

```jsx
import { rest } from 'msw' // msw supports graphql too!
import * as users from './users'

const handlers = [
  rest.get('/user/:uuid', async (req, res, ctx) => {
    if (req.uuid) {
      const user = {
        name: 'Alen',
        email: 'alen@gmail.com',
        location: 'America',
      };
      return res(
        ctx.status(200),
        ctx.json(user)
      );
    }

    return res(
      ctx.status(404),
      ctx.json({ error: 'User not found' }
    ),
  }),
];

export { handlers };
```

&nbsp;

## Testing SWR

It is also worth mentioning that recently there is a new fetch api mechanism called **SWR** (stale while revalidate),
the trending fetching api libraries include:
- [React Query](https://react-query-v3.tanstack.com/overview)
- [RTK Query](https://redux-toolkit.js.org/rtk-query/overview)
- [SWR](https://swr.vercel.app/)

They all use this mechanism, and they are all packaged with hooks to call the api.

It is no longer a simple api function, and we have to do mocking for hooks,
which is not an ideal way.

```jsx
// Testing with swr by manual mock hook

import useSWR from 'swr';
import { render } from '@/utils/testing/render';

import UserStatic, { idUserNumber } from './_userStatic';

jest.mock('swr', () => jest.fn());

describe('UserStatic', () => {
  test('when users data exist, should show correct users number', async () => {
    // Arrange
    const users = [
      { name: 'Alen', email: 'alen@trendmicro.com', },
      { name: 'Benson', email: 'benson@trendmicro.com' },
      { name: 'Camillie', email: 'camillie@trendmicro.com' },
    ];
	
    useSWR.mockResolvedValueOnce({
      data: users,
      isLoading: false,
    });
	  
    // Act
    const { findByTestId } = render(<UserStatic />);
    const userNumber = await findByTestId(idUserNumber);
	  
    // Assert
    expect(userNumber).toHaveTextContent('3');
  });
});
```

&nbsp;

If we use msw to mock api service,
we can use the same way as the general mock api,
instead of mocking msw in this 'tricky' way.

```jsx
// handlers.js
// handle msw api responses

import { rest } from 'msw';

export const handlers = [
  rest.get('/users/:uuid', (req, res, ctx) => {
    const users = [
      { name: 'Alen', email: 'alen@trendmicro.com', },
      { name: 'Benson', email: 'benson@trendmicro.com' },
      { name: 'Camillie', email: 'camillie@trendmicro.com' },
    ];
	
    return res(
      ctx.status(200),
      ctx.json(users),
    );
  }),
];

export default {};
```

```jsx
// Testing with swr by manual mock hook

import useSWR from 'swr';
import { render } from '@/utils/testing/render';

import UserStatic, { idUserNumber } from './_userStatic';

jest.mock('swr', () => jest.fn());

describe('UserStatic', () => {
  test('when users data exist, should show correct users number', async () => {
    // Act
    const { findByTestId } = render(<UserStatic />);
    const userNumber = await findByTestId(idUserNumber);
	  
    // Assert
    expect(userNumber).toHaveTextContent('3');
  });
});
```


&nbsp;

## Conclusion

Using fake service (like `msw`) to mocking apis could
1. Reducing unnecessary mocking duplication, increasing **Maintainability**.
2. More realistic for actual user scenario, increasing **Trustworthy**.

&nbsp;
&nbsp;