Testing with MSW

ben66yueh

YuehBen66

Posted on February 11, 2023

Testing with MSW

Problem

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

Duplication of mocking same api

 

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)

 

  • 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

 

(Hook's code)

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

 

(Component's code)

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

While I was researching how to test swr,
I suddenly found an article ( Stop mocking fetch by Kent C. Dodds ) 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.

  

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:

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 };
Enter fullscreen mode Exit fullscreen mode
//test/server.js
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { handlers } from './server-handlers'

const server = setupServer(...handlers)
export { server, rest };
Enter fullscreen mode Exit fullscreen mode
//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())
Enter fullscreen mode Exit fullscreen mode

  

Rethink the purpose of our fake api

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

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

  

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!

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

 

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:

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.

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

 

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.

// 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 {};
Enter fullscreen mode Exit fullscreen mode
// 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');
  });
});
Enter fullscreen mode Exit fullscreen mode

 

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.

 
 

💖 💪 🙅 🚩
ben66yueh
YuehBen66

Posted on February 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related