2

JavaScript testing #16. Snapshot testing with React, Jest, and Vitest

 4 weeks ago
source link: https://wanago.io/2024/04/08/javascript-testing-snapshots-react-jest-vitest/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

April 8, 2024

This entry is part 16 of 17 in the JavaScript testing tutorial

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
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
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="[email protected]" />)
        .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
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="[email protected]" />,
      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
// 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>
      [email protected]
    </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
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.

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>
        [email protected]
      </p>
    </div>
  </DocumentFragment>
❯ src/UserProfile/UserProfile.test.tsx:10:28
      8|         <UserProfile name="John Smith" email="[email protected]" />,
     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
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
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
// 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
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
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
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
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
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
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
// 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.

Series Navigation<< JavaScript testing #15. Interpreting the code coverage metricJavaScript testing #17. Introduction to End-to-End testing with Playwright >>


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK