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;
};
// 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']);
});
});
(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
};
// 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 ) 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 };
//test/server.js
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { handlers } from './server-handlers'
const server = setupServer(...handlers)
export { server, rest };
//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())
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;
};
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');
});
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 };
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');
});
});
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 {};
// 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');
});
});
Conclusion
Using fake service (like msw
) to mocking apis could
- Reducing unnecessary mocking duplication, increasing Maintainability.
- More realistic for actual user scenario, increasing Trustworthy.
Top comments (0)