- 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
- 15. JavaScript testing #15. Interpreting the code coverage metric
- 16. JavaScript testing #16. Snapshot testing with React, Jest, and Vitest
- 17. JavaScript testing #17. Introduction to End-to-End testing with Playwright
- 18. JavaScript testing #18. E2E Playwright tests for uploading and downloading files
In the fourth part of this series, we’ve learned the basics of mocking API calls. However, there are often situations where we would like to test various more demanding cases. So, in this article, we implement more advanced examples on how to mock with Jest.
Creating a simple React component
Let’s start by creating a straightforward React component that renders a list of posts.
Posts.tsx
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 React from 'react'; import usePostsLoading from './usePostsLoading'; const Posts = () => { const { isLoading, posts, hasFailed } = usePostsLoading(); if (hasFailed) { return <div data-testid="posts-error">Something went wrong</div>; } if (isLoading) { return <div data-testid="posts-loading">Loading...</div>; } if (posts?.length === 0) { return <div data-testid="posts-empty">There are no posts to display</div>; } return ( <div data-testid="posts-container"> {posts?.map((post) => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.body}</p> </div> ))} </div> ); }; export default Posts; |
The logic of storing the data in the state resides in the usePostsLoading hook.
usePostsLoading.tsx
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 |
import { useEffect, useState } from 'react'; import fetchPosts from './fetchPosts'; interface Post { id: number; title: string; body: string; } function usePostLoading() { const [isLoading, setIsLoading] = useState(false); const [hasFailed, setHasFailed] = useState(false); const [posts, setPosts] = useState<Post[] | null>(null); useEffect(() => { setIsLoading(true); setHasFailed(false); fetchPosts() .then((fetchedPosts: Post[]) => { setPosts(fetchedPosts); }) .catch(() => { setHasFailed(true); }) .finally(() => { setIsLoading(false); }); }, []); return { posts, isLoading, hasFailed, }; } export default usePostLoading; |
The fetchPosts function takes care of fetching the data. It is the one we will mock in this article.
fetchPosts.tsx
1 2 3 4 5 6 7 8 9 10 11 12 |
function fetchPosts() { return fetch('https://jsonplaceholder.typicode.com/posts').then( (postsResponse) => { if (postsResponse.ok) { return postsResponse.json(); } return Promise.reject(); }, ); } export default fetchPosts; |
Mocking a function
In the fourth part of this series, we’ve defined mocks in the __mocks__ directory. Instead, we can mock a function at the top of our test file.
Mocking a named export
Let’s rewrite our fetchPosts.tsx file to use a named export for a moment.
fetchPosts.tsx
1 2 3 4 5 6 7 8 9 10 11 12 |
function fetchPosts() { return fetch('https://jsonplaceholder.typicode.com/posts').then( (postsResponse) => { if (postsResponse.ok) { return postsResponse.json(); } return Promise.reject(); }, ); } export { fetchPosts }; |
With the above approach, it is straightforward to mock a module.
1 2 3 |
jest.mock('./fetchPosts', () => ({ fetchPosts: jest.fn().mockReturnValue(Promise.resolve([])), })); |
Above, we use jest.mock() function that creates a mock function. For example, we can use it to change the value that a function returns. In our case, we force the fetchPosts function to return a promise that resolves to an empty array.
Mocking a default export
Our original fetchPosts.tsx file uses a default export. Therefore, we need to modify our mock a bit.
1 2 3 4 |
jest.mock('./fetchPosts', () => ({ __esModule: true, default: jest.fn().mockReturnValue(Promise.resolve([])), })); |
We can simplify the above mock a little by using the mockResolvedValue method.
Posts.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { render } from '@testing-library/react'; import React from 'react'; import Posts from './Posts'; jest.mock('./fetchPosts', () => ({ __esModule: true, default: jest.fn().mockResolvedValue([]), })); describe('The Posts component', () => { describe('when the component fetches an empty array of posts', () => { it('should render the no posts indicator', async () => { const posts = render(<Posts />); await posts.findByTestId('posts-empty'); }); }); }); |
The fetchPosts returns a promise that resolves to an empty array thanks to the above. Because of that, the Posts component does not make an actual HTTP request.
A crucial thing to notice in our test is that our fetchPosts function is still asynchronous. Since we use await posts.findByTestId('posts-empty'), our test waits up to 1000ms for the <div data-testid="posts-empty" /> to appear. If the React Testing Library does not find the element during that time, it throws an error, and our test fails.
Mocking a function differently per test
Above, we create a mocked version of the fetchPosts function that always returns the same value. Instead, we might want to mock a function differently per test. Let’s start by using the jest.mock() function on top of our test.
1 2 3 4 |
jest.mock('./fetchPosts', () => ({ __esModule: true, default: jest.fn() })); |
Thanks to doing the above, our fetchPosts function is now replaced with a mock function. The crucial thing to acknowledge is that when we import fetchPosts in usePostsLoading.tsx or post.test.tsx, we import an instance of a mock function. So we can use this fact and interact with our mock through the test to change what it returns in usePostsLoading.tsx.
Posts.test.tsx
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 { render } from '@testing-library/react'; import React from 'react'; import Posts from './Posts'; import fetchPosts from './fetchPosts'; jest.mock('./fetchPosts', () => ({ __esModule: true, default: jest.fn(), })); describe('The Posts component', () => { describe('when the component fetches an empty array of posts', () => { beforeEach(() => { (fetchPosts as jest.Mock).mockResolvedValue([]); }); it('should render the no posts indicator', async () => { const posts = render(<Posts />); await posts.findByTestId('posts-empty'); }); }); describe('when the posts fetching fails', () => { beforeEach(() => { (fetchPosts as jest.Mock).mockRejectedValue(null); }); it('should render the error indicator', async () => { const posts = render(<Posts />); await posts.findByTestId('posts-error'); }); }); }); |
Since the fetchPosts function that we import on top of our test now has a value of jest.fn(), we can call mockResolvedValue and mockRejectedValue on it. We can achieve a different mocked value per test by manipulating the fetchPosts mock before each test.
Mocking a React component
When writing tests with Enzyme, we can choose to either render a component with all of its children or perform a shallow render. In contrast, we don’t have this option with React Testing Library.
The idea behind React Testing Library is to write tests that resemble the way the users interact with our application. Because of that, mocking React components is discouraged. Even though that’s the case, we sometimes might want to avoid testing some components as a whole. The above might happen when using third-party libraries, for example.
DataViewer.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import React, { FunctionComponent } from 'react'; import ReactJson from 'react-json-view'; interface Props { data: unknown; } const DataViewer: FunctionComponent<Props> = ({ data }) => { if (data !== null && typeof data === 'object') { return <ReactJson src={data} />; } return <p>{data.toString()}</p>; }; export default DataViewer; |
In our test, let’s find out if the DataViewer renders the ReactJson component when provided with a dictionary.
DataViewer.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import DataViewer from './DataViewer'; import { render } from '@testing-library/react'; jest.mock('react-json-view', () => ({ __esModule: true, default: () => <div data-testid="react-json-view" />, })); describe('The DataViewer component', () => { describe('when provided with a dictionary', () => { it('should render a ReactJson component', async () => { const dataViewer = render(<DataViewer data={{ key: 'value' }} />); await dataViewer.findByTestId('react-json-view'); }); }); describe('when provided with a string', () => { it('should render a paragraph with the provided string', async () => { const text = 'value'; const dataViewer = render(<DataViewer data={text} />); await dataViewer.findByText(text, { selector: 'p' }); }); }); }); |
Without mocking the react-json-view library above, we wouldn’t have a straightforward way of checking whether our component rendered it when using React Testing Library.
Mocking a module partially
So far, we’ve constantly mocked a whole module when using jest.mock(). We might not want to do that in every case. Let’s imagine having the following file:
utilities.tsx
1 2 3 4 5 6 7 8 9 |
function sum(firstNumber: number, secondNumber: number) { return firstNumber + secondNumber; } function subtract(firstNumber: number, secondNumber: number) { return firstNumber - secondNumber; } export { sum, subtract } |
If we want to mock only the sum function, we can pair jest.mock() with jest.requireActual(). When we call jest.requireActual(), we can retrieve the original content of a module even if we mocked it.
1 2 3 4 |
jest.mock('./utilities', () => ({ ...jest.requireActual('./utilities'), sum: jest.fn(), })); |
Summary
In this article, we’ve gone through various use-cases of mocking functions. This included real-life situations such as mocking a function differently per test, mocking a default export, or mocking a React component. We’ve also learned how to mock a module partially. All of the above provides a cheat sheet that might come in handy in various cases.
I’d recommend using msw for mocking APIs, in your case “fetchPosts”
How’d you mock a function that declared inside a react component and is used inside it’s own view ?
not sure if you can, perhaps you could abstract it out to its function from another module or pass it as a React component prop, both of which can be mocked