- 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
Unit testing is handy for testing how our React components behave when the user interacts with them. However, this might not be enough. Besides testing the logic behind a particular component, we should also test if our user interface changes unexpectedly. We can use snapshot testing to detect unintended modifications to how our components render.
Introduction to snapshot testing
Let’s create a straightforward React component so that we can test it. Its job is to render a user’s name and email.
UserProfile.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { FunctionComponent } from 'react'; interface Props { name: string; email: string; } export const UserProfile: FunctionComponent<Props> = ({ name, email }) => { return ( <div> <p>{name}</p> <p>{email}</p> </div> ); }; |
In our test, we need to render our component and use the expect().toMatchSnapshot() function. There are a few ways to do that.
The official Jest docs mention the react-test-renderer library that renders our component and provides its representation in JSON.
UserProfile.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { UserProfile } from './UserProfile'; import renderer from 'react-test-renderer'; describe('The UserProfile component', () => { describe('when a valid name and email are provided', () => { it('should render correctly', () => { const result = renderer .create(<UserProfile name="John Smith" email="john@smith.com" />) .toJSON(); expect(result).toMatchSnapshot(); }); }); }); |
However, there is a high chance that you already use the React Testing Library, the most popular solution for testing React components. It provides the asFragment function, which returns a document fragment with the current state of our component’s rendered output.
UserProfile.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { render } from '@testing-library/react'; import { UserProfile } from './UserProfile'; describe('The UserProfile component', () => { describe('when a valid name and email are provided', () => { it('should render correctly', () => { const { asFragment } = render( <UserProfile name="John Smith" email="john@smith.com" />, ); expect(asFragment()).toMatchSnapshot(); }); }); }); |
How the snapshot is created
The first time we run our test, the toMatchSnapshot() function creates a snapshot file in the __snapshots__ directory.
UserProfile.test.tsx.snap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`The UserProfile component > when a valid name and email are provided > should render correctly 1`] = ` <DocumentFragment> <div> <p> John Smith </p> <p> john@smith.com </p> </div> </DocumentFragment> `; |
If we’re using Jest instead of Vitest, the first line in the file will mention Jest.
The snapshot file contains the output of our React component at the time we called the asFragment() function. We should commit the snapshot file to our repository and treat it as an integral part of our test.
Updating snapshots
Let’s modify our component slightly.
UserProfile.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { FunctionComponent } from 'react'; interface Props { name: string; email: string; } export const UserProfile: FunctionComponent<Props> = ({ name, email }) => { return ( <div> <h2>{name}</h2> <p>{email}</p> </div> ); }; |
Above, we changed one of the paragraphs to <h2>. When we re-run our tests, the toMatchSnapshot() function compares the modified component with the snapshot we saved previously. This will fail because the rendered component does not match the snapshot.
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 |
FAIL src/UserProfile/UserProfile.test.tsx > The UserProfile component > when a valid name and email are provided > should render correctly Error: Snapshot `The UserProfile component > when a valid name and email are provided > should render correctly 1` mismatched - Expected + Received <DocumentFragment> <div> - <p> + <h2> John Smith - </p> + </h2> <p> john@smith.com </p> </div> </DocumentFragment> ❯ src/UserProfile/UserProfile.test.tsx:10:28 8| <UserProfile name="John Smith" email="john@smith.com" />, 9| ); 10| expect(asFragment()).toMatchSnapshot(); | ^ 11| }); 12| }); |
When this happens, our testing framework will ask us if we want to update the snapshot. If we do, we will have to commit it to the repository again. We will have to do that every time we make changes to our code that affect how the component is rendered.
Avoiding non-deterministic tests
Let’s create a component that displays the current date.
CurrentDate.tsx
1 2 3 4 5 6 7 8 9 10 11 |
export const CurrentDate = () => { const currentDate = new Date(); const formattedDate = currentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); return <div>{formattedDate}</div>; }; |
First, let’s write a simple snapshot test for it using the knowledge we already have.
CurrentDate.test.tsx
1 2 3 4 5 6 7 8 9 |
import { render } from '@testing-library/react'; import { CurrentDate } from './CurrentDate'; describe('The CurrentDate component', () => { it('should render correctly', () => { const { asFragment } = render(<CurrentDate />); expect(asFragment()).toMatchSnapshot(); }); }); |
When we look at the snapshot file created, we can see that it contains the current date.
CurrentDate.test.tsx.snap
1 2 3 4 5 6 7 8 9 |
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`The CurrentDate component > should render correctly 1`] = ` <DocumentFragment> <div> April 6, 2024 </div> </DocumentFragment> `; |
Unfortunately, this makes our test non-deterministic. If we run it tomorrow, it will fail. To deal with that, we should mock the current date. Fortunately, both Vitest and Jest support it.
If you want to know more about mocking, check out the following articles:
JavaScript testing #10. Advanced mocking with Jest and React Testing Library
JavaScript testing #13. Mocking a REST API with the Mock Service Worker
CurrentDate.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { render } from '@testing-library/react'; import { CurrentDate } from './CurrentDate'; import { vi } from 'vitest'; describe('The CurrentDate component', () => { beforeEach(() => { vi.useFakeTimers().setSystemTime(new Date('2024-04-06')); }); afterEach(() => { vi.useRealTimers(); }); it('should render correctly', () => { const { asFragment } = render(<CurrentDate />); expect(asFragment()).toMatchSnapshot(); }); }); |
When using Jest, use jest.useFakeTimers().setSystemTime() and jest.useRealTimers() instead.
Thanks to the above approach, our test will always use the same date, no matter when we run it.
Loading data asynchronously
Let’s create a simple component fetching a list of posts from a REST API.
Posts.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { usePosts } from './usePosts'; export const Posts = () => { const { posts } = usePosts(); if (!posts) { return <div>Loading...</div>; } return ( <div> {posts.map((post) => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.body}</p> </div> ))} </div> ); }; |
The usePosts custom hook loads the data asynchronously through the useEffect hook.
usePosts.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useEffect, useState } from 'react'; import { fetchPosts, Post } from './fetchPosts'; export function usePosts() { const [posts, setPosts] = useState<Post[] | null>(null); useEffect(() => { fetchPosts().then((fetchedPosts) => { setPosts(fetchedPosts); }); }, []); return { posts, }; } |
It uses the fetchPosts function under the hood to fetch the data.
fetchPosts.tsx
1 2 3 4 5 6 7 8 9 10 |
export interface Post { id: number; title: string; body: string; } export async function fetchPosts(): Promise<Post[]> { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); return response.json(); } |
Thanks to that, we can easily mock the fetchPosts function.
First, let’s create a snapshot when the posts are still loading.
Posts.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { render } from '@testing-library/react'; import { Posts } from './Posts'; import { beforeEach } from 'vitest'; import { fetchPosts } from './fetchPosts'; import { vi, Mock } from 'vitest'; vi.mock('./fetchPosts', () => ({ fetchPosts: vi.fn(), })); describe('The Posts component', () => { describe('when the posts are loading', () => { beforeEach(() => { (fetchPosts as Mock).mockReturnValue(new Promise(() => null)); }); it('should render correctly', () => { const { asFragment } = render(<Posts />); expect(asFragment()).toMatchSnapshot(); }); }); }); |
Second, let’s create a second snapshot when the posts are already loaded. In this case, calling the asFragment() function at the right moment is crucial. We need to wait for the posts to be loaded.
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 33 34 35 36 37 38 |
import { render, waitFor } from '@testing-library/react'; import { Posts } from './Posts'; import { beforeEach } from 'vitest'; import { fetchPosts } from './fetchPosts'; import { vi, Mock } from 'vitest'; vi.mock('./fetchPosts', () => ({ fetchPosts: vi.fn(), })); describe('The Posts component', () => { // ... describe('when the posts are loaded correctly', () => { beforeEach(() => { (fetchPosts as Mock).mockResolvedValue([ { id: 1, title: 'The first post', body: 'Hello world!', }, { id: 2, title: 'The second post', body: 'Hello world!', }, ]); }); it('should render correctly', async () => { const { asFragment, queryByText } = render(<Posts />); await waitFor(() => { return expect(queryByText('Loading...')).toBe(null); }); expect(asFragment()).toMatchSnapshot(); }); }); }); |
Running this test suite creates a snapshot file that contains two snapshots.
Posts.test.tsx.snap
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 |
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`The Posts component > when the posts are loaded correctly > should render correctly 1`] = ` <DocumentFragment> <div> <div> <h2> The first post </h2> <p> Hello world! </p> </div> <div> <h2> The second post </h2> <p> Hello world! </p> </div> </div> </DocumentFragment> `; exports[`The Posts component > when the posts are loading > should render correctly 1`] = ` <DocumentFragment> <div> Loading... </div> </DocumentFragment> `; |
Summary
In this article, we’ve discussed snapshot tests and implemented them in simple and more advanced scenarios that involved mocking and asynchronous data loading.
Snapshot testing can help ensure that the user interface does not change unexpectedly. It can also come in handy in situations where we have complex components and tracking the changes manually would be especially difficult. Therefore, incorporating snapshot testing can make our codebase more reliable and maintainable by making it easier to catch any unintended changes early in the development process.