Unit testing has become an integral part of application development, and in this discussion, we will be talking about mocking API while unit testing using MSW, a tool used for mocking APIs. We also won't be setting the test environment up since we expect you have already done so, and also we will be using a React project for our testing.
Please note: the setup will work for any framework of your choice.
What is MSW?
MSW (Mock Service Worker) is an API mocking library that uses Service Worker API to intercept actual requests.
Why Mock?
Well, it avoids us making an actual HTTP request by leveraging on mock server, and a service worker which, in turn, prevents any form of break should something go wrong with the server you would have sent a request to.
What is a Mock Server?
A mock server imitates a real API server by returning the mock API responses to the API requests.
This will help intercept the API request.
You can read more here.
Project Setup
We will be installing few packges, and doing a little configuration.
Installations
We will be installing the following
- msw: This package helps use to mock the API request and also helps us set up the server.
- whatwg-fetch: Not always required, but what it does is it makes fetch available if you are useing the fetch API. Sometimes you get this:
# using npm
npm install -D msw whatwg-fetch
# using yarn
yarn add -D msw whatwg-fetch
Creating the mock config
Lets create a mock folder in our src folder to keep things organized.
We want to have two files in the mock folder in this example:
- postHandler.js: Here, we will have the handler that will help with intercepting the API.
- serverSetup.js: Here we will set up or server that will house the handler.
postHandler.js content
Its important that the API url matches the url you intend to intercept:
import { rest } from 'msw';
const posts = [{
title: 'title a',
body: 'body a'
}];
const postHandler = rest.get('https://jsonplaceholder.typicode.com/posts', (_, res, ctx) => {
return res(
ctx.status(200),
ctx.json(posts)
)
});
export default postHandler;
Lets explain the above code:
posts: This is dummy data that is going to serve as our response data knowing full well what the data structure might look like. postHandler: This will serve as the handler, which will help in intercepting the request made to that url.
serverSetUp.js content
import { setupServer } from 'msw/node';
import postHandler from './postHandler';
const server = setupServer(postHandler,/*anotherHandler*/)
export {
server,
}
From the above code, the setupServer can take in any number of handlers separated by comma. The setupServer
serves as our "fake"
server where we can add as many handlers to intercept requests.
Component
Lets create a component for the purpose of our test.
Here, I will be making use of the App.js file.
My code will look like this:
import { useEffect, useState } from 'react';
function App() {
const [posts, setPosts] = useState([])
const [loadingData, setLoadingData] = useState(true);
useEffect(() => {
const dataFunction = async() => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts")
const dataJson = await res.json();
const fetchData = dataJson.slice(0, 4);
setPosts(fetchData);
setLoadingData(false)
}
dataFunction();
}, [])
return (
<div className="App">
{loadingData ? <div>loading...</div> : (
<ul data-testid="posts" >
{posts.map((post, i) => (
<li key={i}>
<h5>{post.title}</h5>
<p>{post.body}</p>
</li>
))}
</ul>
)}
</div>
);
}
export default App;
All we are doing is fetching some data from the URL when the component mounts and saving the data in our state to display them. And we are using the jsonplacehoder url to fetch posts.
Test the component
We will create a test file named App.spec.js with this contents
import {
render,
screen,
} from '@testing-library/react';
import 'whatwg-fetch';
import App from './App';
import { server } from './mock/serverSetUp';
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
test('renders App and fetch data', async () => {
await render(<App />);
const loading = await screen.findByText('loading...');
expect(loading).toBeInTheDocument();
const listNode = await screen.findByTestId('posts')
expect(loading).not.toBeInTheDocument();
expect(listNode.childNodes).toHaveLength(1);
});
We imported the component we want to test with: the whatwg-fetch
to prevent us with the possible error we talked about. Then, we imported our server.
There are few methods above:
- beforeAll: This runs before all tests are executed.
- afterAll: The opposite of beforeAll.
- afterEach: As the name implies, it runs after each test.
- resetHandler: It is a useful clean up mechanism between multiple test suites that leverage runtime request handlers. If you added another handler during a test, this resetHandler will remove that handler so that the next test will not know about it. You can see an example of reset handler. Though, in our code example, we didn't need it. But its important to know we have such control and power over what we want to do.
So, in summary before all listen to the server for any request, after all close the server and after each test reset handler.
On app render there is an API call, but our server will intercept it, so at first the loading text will exist. Then after, it won't.
Will it work without intercepting? Yes it will.
So, why do all of this?
- Anything could happen to the API server, so you don't want you app test to fail.
- The data could change, and we don't want a situation where the test fails because of data changes.
Conclusion
We have created a repo that you can test with in case you have issues following along. Please feel free to let us know what you think as we look forward to building a stronger community by sharing knowledge.