- 1. JavaScript testing #1. Explaining types of tests. Basics of unit testing with Jest
- 2. JavaScript testing #2. Introducing Enzyme and testing React components
- 3. JavaScript testing #3. Testing props, the mount function and snapshot tests.
- 4. JavaScript testing #4. Mocking API calls and simulating React components interactions
- 5. JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux
- 6. JavaScript testing #6. Introduction to End-to-End testing with Cypress
- 7. JavaScript testing #7. Diving deeper into commands and selectors in Cypress
- 8. JavaScript testing #8. Integrating Cypress with Cucumber and Gherkin
- 9. JavaScript testing #9. Replacing Enzyme with React Testing Library
- 10. JavaScript testing #10. Advanced mocking with Jest and React Testing Library
- 11. JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks
- 12. JavaScript testing #12. Testing downloaded files and file inputs with Cypress
- 13. JavaScript testing #13. Mocking a REST API with the Mock Service Worker
- 14. JavaScript testing #14. Mocking WebSockets using the mock-socket library
One of the most crucial things about writing tests is that they should be deterministic. A particular test should always succeed or always fail. To achieve that, we sometimes need to mock parts of our environment when testing. A good example can be a function that relies on pseudo-random numbers.
Besides the above, we also want our frontend unit tests to be independent of the backend. If there is a bug in our API, we don’t want it to cause frontend tests to fail too. To achieve that, we could mock functions we use to make HTTP requests, such as fetch() and axios.get(). While that would work fine, switching from using fetch to axios or the other way around would force us to adjust our mocks. So instead, the Mock Service Worker library offers a different approach.
Introducing Mock Service Worker (MSW)
The Mock Service Worker library offers two major features. One of them is mocking the API requests in the browser.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import { rest, setupWorker } from 'msw'; const worker = setupWorker( rest.get( 'https://jsonplaceholder.typicode.com/posts', (request, response, context) => { return response( context.json([ { id: 1, title: 'First post', body: 'This is the first post', }, { id: 2, title: 'Second post', body: 'This is the second post', }, ]), ); }, ), ); worker.start() |
In the callback for the rest.get function, we have three arguments:
- request: an object that stores the information about the matched request,
- response: a function that creates a mocked response object,
- context: an object containing a set of response transformers that help us compose a response.
The response function accepts the response transformers as arguments. Each response transformer modifies the response. Besides using the response transformers provided by the context object, we can create our own.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
rest.get( 'https://jsonplaceholder.typicode.com/posts', async (request, response) => { return response( (response) => { response.status = 200; return response; }, (response) => { response.headers.set('Content-Type', 'application/json') return response; }, ); }, ), |
The setupWorker function creates a Service Worker configuration instance and the worker.start function registers and activates it. Doing the above causes the MSW library to intercept the /posts API request through a Service Worker and return the response we’ve defined. This means we can open the developer tools and see the browser making the API requests. It is a significant improvement from mocking the fetch() function.
If you want to know more bout Service Workers in general, check out The basics of Service Workers: how should our application behave when offline?
The second major feature that MSW offers is mocking API requests in Node.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import { rest } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( rest.get( 'https://jsonplaceholder.typicode.com/posts', (request, response, context) => { return response( context.json([ { id: 1, title: 'First post', body: 'This is the first post', }, { id: 2, title: 'Second post', body: 'This is the second post', }, ]), ); }, ), ); server.listen() |
We can pass multiple arguments to the setupServer and setupWorker functions to mock multiple requests.
It is essential to realize that Node.js does not have a concept of Service Workers. Because of that, MSW has to intercept functions such as fetch and http.get being called.
The use-cases of client-side and Node.js mocking
The first thing to notice is the API for defining both Node.js and browser mocks is the same. Therefore, sharing the mocks between the browser and Node.js is easy.
Mocking client-side API requests might come in handy in a few scenarios. One of them is when the actual API is not ready yet, but we still want to be able to work on the frontend side of our application. This can be a common case in teams where different developers are working on creating the API and on the frontend.
Mocking API requests in the browser can also be helpful if we don’t want our Cypress tests to use a real API. However, we should know that such tests might not be considered end-to-end tests because they don’t verify all of the aspects of the application.
Writing tests with Jest involves running our frontend application through Node.js. Because of that, Node.js API mocks can be crucial to our unit tests. Therefore, in this article, we focus on writing Node.js mocks.
Mocking API calls in a Node.js environment
We can take two different approaches when setting up mocks. Each of them has its advantages and disadvantages.
Setting up API mocks once for all of the tests
The most straightforward way of mocking API calls with MSW is to configure it once for all of the tests. To do that, we need to define the server in a file included in the setupFilesAfterEnv array in our Jest configuration.
setupTests.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { rest } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( rest.get( 'https://jsonplaceholder.typicode.com/posts', (request, response, context) => { return response( context.json([ { id: 1, title: 'First post', body: 'This is the first post', }, { id: 2, title: 'Second post', body: 'This is the second post', }, ]), ); }, ), ); beforeAll(() => { server.listen(); }); afterAll(() => { server.close(); }); |
In Create React App, setupTests is the default file used for configuring tests.
To verify the above approach, let’s create a straightforward component for displaying a list of posts.
PostsList.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import React from 'react'; import usePostsList from './usePostsList'; const PostsList = () => { const { data, hasFailed } = usePostsList(); if (hasFailed) { return <div>Something went wrong...</div>; } return ( <div> {data?.map((post) => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.body}</p> </div> ))} </div> ); }; export default PostsList; |
Our React component uses a custom hook that sends an HTTP request to the API we’ve mocked.
usePostsList.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import { useEffect, useState } from 'react'; import Post from '../Post'; function usePostsList() { const [hasFailed, setHasFailed] = useState(false); const [data, setData] = useState<Post[] | null>(null); useEffect(() => { setHasFailed(false); fetch('https://jsonplaceholder.typicode.com/posts') .then((response) => { if (response.ok) { return response.json(); } else { throw Error(); } }) .then((postsData) => { setData(postsData); }) .catch(() => { setHasFailed(true); }); }, []); return { data, hasFailed, }; } export default usePostsList; |
Now, let’s write a test to ensure we render the correct data.
PostsList.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { render } from '@testing-library/react'; import PostsList from './PostsList'; describe('The PostsList component', () => { it('should display the tiles of all of the posts', async () => { const postsList = render(<PostsList />); await postsList.findByText('First post'); await postsList.findByText('Second post'); }); it('should display the bodies of all of the posts', async () => { const postsList = render(<PostsList />); await postsList.findByText('This is the first post'); await postsList.findByText('This is the second post'); }); }); |
PASS src/PostsList/PostsList.test.tsx
The PostsList component
✓ should display the tiles of all of the posts
✓ should display the bodies of all of the posts
Implementing the above approach has some advantages. It is straightforward to use and keeps our tests short and easy to understand. Unfortunately, it does not give us much control over the API mocks. Our PostsList component can display an error message if the API request fails. Unfortunately, we currently don’t have a way of testing that.
Setting up API mocks per test
Instead of setting API mocks once for all of the tests, we can do it once per test instead. It gives us a lot more control over how our API mocks work.
PostsList.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
import { render } from '@testing-library/react'; import PostsList from './PostsList'; import { setupServer, SetupServerApi } from 'msw/node'; import { rest } from 'msw'; import Post from '../Post'; describe('The PostsList component', () => { let server: SetupServerApi; afterEach(() => { server.close(); }); describe('if fetching posts is a success', () => { let posts: Post[]; beforeEach(() => { posts = [ { id: 1, title: 'First post', body: 'This is the first post', }, { id: 2, title: 'Second post', body: 'This is the second post', }, ]; server = setupServer( rest.get( 'https://jsonplaceholder.typicode.com/posts', (request, response, context) => { return response(context.json(posts)); }, ), ); server.listen(); }); it('should display the titles of all of the posts', async () => { const postsList = render(<PostsList />); for (const post of posts) { await postsList.findByText(post.title); } }); it('should display the bodies of all of the posts', async () => { const postsList = render(<PostsList />); for (const post of posts) { await postsList.findByText(post.body); } }); }); describe('if fetching posts fails', () => { beforeEach(() => { server = setupServer( rest.get( 'https://jsonplaceholder.typicode.com/posts', (request, response, context) => { return response( context.status(500), context.json({ errorMessage: 'Something went wrong', }), ); }, ), ); server.listen(); }); it('should display an error message', async () => { const postsList = render(<PostsList />); await postsList.findByText('Something went wrong...'); }); }); }); |
PASS src/PostsList/PostsList.test.tsx
The PostsList component
if fetching posts is a success
✓ should display the titles of all of the posts
✓ should display the bodies of all of the posts
if fetching posts fails
✓ should display an error message
In the above test, we can mock the response from the same endpoint in more than one way. Thanks to that, we can test how our React component behaves in many different scenarios.
While this approach is more powerful, it makes our tests noticeably more complicated. Also, starting the server multiple times adds to the execution time of our tests. The additional control seems to be worth it, though.
Intercepting the request
We sometimes want to assert the request payload, for example, to ensure that our frontend made a valid HTTP request to create a post. To test it, let’s create a simple form.
PostsForm.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import React from 'react'; import usePostsForm from './usePostsForm'; const PostsForm = () => { const { handleSubmit } = usePostsForm(); return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="title" aria-label="title-input" /> <input name="body" placeholder="body" aria-label="body-input" /> <button>Create</button> </form> ); }; export default PostsForm; |
When the user clicks the button, we send a POST request to the API.
usePostsForm.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import { FormEvent } from 'react'; function usePostsForm() { const handleSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const { title, body } = Object.fromEntries(formData); fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title, body, }), }); }; return { handleSubmit, }; } export default usePostsForm; |
One way to test the above logic would be to create a variable in the test that we modify in our route handler.
PostsForm.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import { fireEvent, render, waitFor } from '@testing-library/react'; import { setupServer, SetupServerApi } from 'msw/node'; import { rest } from 'msw'; import PostsForm from './PostsForm'; describe('The PostsForm component', () => { let server: SetupServerApi; let createPostPayload: { title: string; body: string; }; beforeEach(() => { server = setupServer( rest.post( 'https://jsonplaceholder.typicode.com/posts', async (request, response, context) => { createPostPayload = await request.json(); return response(context.status(201)); }, ), ); server.listen(); }); afterEach(() => { server.close(); }); describe('when the user types valid values for body and title', () => { describe('and when the user clicks on the submit button', () => { it('should make a POST request with the form data', async () => { const postsForm = render(<PostsForm />); const title = 'New post title'; const body = 'New post body'; const titleInput = await postsForm.findByLabelText('title-input'); fireEvent.change(titleInput, { target: { value: title } }); const bodyInput = await postsForm.findByLabelText('body-input'); fireEvent.change(bodyInput, { target: { value: body } }); const submitButton = await postsForm.getByRole('button'); fireEvent.click(submitButton); return waitFor(() => { return expect(createPostPayload).toEqual({ title, body, }); }); }); }); }); }); |
PASS src/PostsForm/PostsForm.test.tsx
The PostsForm component
when the user types valid values for body and title
and when the user clicks on the submit button
✓ should make a POST request with the form data
We can also use the life cycle events built into MSW to achieve a similar result.
PostsForm.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import { fireEvent, render, waitFor } from '@testing-library/react'; import { setupServer, SetupServerApi } from 'msw/node'; import { matchRequestUrl, rest } from 'msw'; import PostsForm from './PostsForm'; describe('The PostsForm component', () => { let server: SetupServerApi; let createPostPayload: { title: string; body: string; }; beforeEach(() => { const postsUrl = 'https://jsonplaceholder.typicode.com/posts'; server = setupServer( rest.post( 'https://jsonplaceholder.typicode.com/posts', (request, response, context) => { return response(context.status(201)); }, ), ); server.events.on('request:match', async (request) => { if (request.method === 'POST' || matchRequestUrl(request.url, postsUrl).matches) { createPostPayload = await request.json(); } }); server.listen(); }); afterEach(() => { server.close(); }); describe('when the user types valid values for body and title', () => { describe('and when the user clicks on the submit button', () => { it('should make a POST request with the form data', async () => { const postsForm = render(<PostsForm />); const title = 'New post title'; const body = 'New post body'; const titleInput = await postsForm.findByLabelText('title-input'); fireEvent.change(titleInput, { target: { value: title } }); const bodyInput = await postsForm.findByLabelText('body-input'); fireEvent.change(bodyInput, { target: { value: body } }); const submitButton = await postsForm.getByRole('button'); fireEvent.click(submitButton); return waitFor(async () => { return expect(createPostPayload).toEqual({ title, body, }); }); }); }); }); }); |
The official documentation shows also shows how to create a waitForRequest function.
Summary
In this article, we’ve gone through the Mock Service Worker library. We’ve learned about different use cases for it – both in the browser and through Node.js. We’ve also used it in various testing scenarios using React Testing Library and Jest. This included learning different ways to set up API mocks to match our needs. Thanks to practicing all of the above, we’ve grasped the basics of working with MSW.