Learn Jest in 10 DaysDay 7: Snapshot Testing

Day 7: Snapshot Testing

What You'll Learn Today

  • What snapshot testing is and when to use it
  • Using toMatchSnapshot() for basic snapshot tests
  • Using toMatchInlineSnapshot() for inline snapshots
  • Updating snapshots with --updateSnapshot / -u
  • Creating custom serializers
  • Snapshot testing for non-UI data (API responses, config objects)
  • Best practices and common pitfalls

What Is Snapshot Testing?

Snapshot testing captures the output of a value, saves it to a file, and compares future outputs against that saved "snapshot." It automatically detects unintended changes in your code's output.

flowchart TB
    subgraph First["First Run"]
        T1["Run test"] --> S1["Generate snapshot"]
        S1 --> F1["Save to\n__snapshots__/ file"]
    end
    subgraph Later["Subsequent Runs"]
        T2["Run test"] --> S2["Generate new output"]
        S2 --> CMP["Compare with\nsaved snapshot"]
        CMP -->|"Match"| PASS["Test passes ✓"]
        CMP -->|"Mismatch"| FAIL["Test fails ✗"]
    end
    style First fill:#3b82f6,color:#fff
    style Later fill:#8b5cf6,color:#fff

When to Use Snapshot Testing

Good Fit Poor Fit
UI component output Business logic validation
API response structure Frequently changing data
Config object change detection Dynamic values (dates, random IDs)
Error message consistency Huge data structures

toMatchSnapshot() Basics

toMatchSnapshot() saves a value to an external snapshot file and compares against it on subsequent runs.

// formatUser.js
function formatUser(user) {
  return {
    displayName: `${user.firstName} ${user.lastName}`,
    email: user.email.toLowerCase(),
    role: user.role || 'member',
  };
}

module.exports = { formatUser };
// formatUser.test.js
const { formatUser } = require('./formatUser');

test('formats user correctly', () => {
  const user = {
    firstName: 'Alice',
    lastName: 'Smith',
    email: 'Alice@Example.com',
    role: 'admin',
  };

  expect(formatUser(user)).toMatchSnapshot();
});

TypeScript version:

// formatUser.ts
interface User {
  firstName: string;
  lastName: string;
  email: string;
  role?: string;
}

interface FormattedUser {
  displayName: string;
  email: string;
  role: string;
}

export function formatUser(user: User): FormattedUser {
  return {
    displayName: `${user.firstName} ${user.lastName}`,
    email: user.email.toLowerCase(),
    role: user.role || 'member',
  };
}

On the first run, Jest auto-generates __snapshots__/formatUser.test.js.snap:

// Jest Snapshot v1, https://goo.gl/fbAXQV

exports[`formats user correctly 1`] = `
{
  "displayName": "Alice Smith",
  "email": "alice@example.com",
  "role": "admin",
}
`;

Multiple Snapshots

When a single test file contains multiple snapshots, they are automatically numbered.

test('formats different user roles', () => {
  const admin = formatUser({ firstName: 'Alice', lastName: 'Smith', email: 'a@b.com', role: 'admin' });
  const member = formatUser({ firstName: 'Bob', lastName: 'Jones', email: 'b@c.com' });

  // saved as "formats different user roles 1"
  expect(admin).toMatchSnapshot();
  // saved as "formats different user roles 2"
  expect(member).toMatchSnapshot();
});

Tip: You can add a hint for readability: expect(admin).toMatchSnapshot('admin user')


toMatchInlineSnapshot()

toMatchInlineSnapshot() embeds the snapshot directly in your test code. This is ideal for small snapshots since you can see the expected value without opening a separate file.

test('formats user inline', () => {
  const user = formatUser({
    firstName: 'Alice',
    lastName: 'Smith',
    email: 'Alice@Example.com',
    role: 'admin',
  });

  // first run: Jest automatically fills in the snapshot
  expect(user).toMatchInlineSnapshot(`
    {
      "displayName": "Alice Smith",
      "email": "alice@example.com",
      "role": "admin",
    }
  `);
});

On the first run, if toMatchInlineSnapshot() is called with an empty argument, Jest automatically inserts the snapshot into your test file.

flowchart LR
    subgraph External["toMatchSnapshot()"]
        E1["Saves snapshot\nto external file"]
        E2["__snapshots__/\n*.snap"]
    end
    subgraph Inline["toMatchInlineSnapshot()"]
        I1["Embeds snapshot\nin test code"]
        I2["Test file itself\nis updated"]
    end
    style External fill:#3b82f6,color:#fff
    style Inline fill:#22c55e,color:#fff
Comparison toMatchSnapshot() toMatchInlineSnapshot()
Storage __snapshots__/*.snap Inside the test file
Readability Requires opening separate file Visible alongside the test
Best for Larger snapshots Small snapshots
Code review Changes less visible Diffs are clear

Updating Snapshots

When code changes intentionally alter the output, you need to update the saved snapshots.

CLI Options

# update all snapshots
npx jest --updateSnapshot
# or shorthand
npx jest -u

# update snapshots for a specific test file
npx jest formatUser.test.js -u

Interactive Mode (Watch Mode)

When running jest --watch, press u to update failing snapshots:

Snapshot Summary
 › 1 snapshot failed from 1 test suite.
   › Press `u` to update failing snapshots.

Caution: Always review the diff before updating. Blindly updating snapshots defeats the purpose of snapshot testing.


Handling Dynamic Values

For values that change on every run (dates, IDs, random values), use property matchers.

test('creates a user with dynamic fields', () => {
  const user = {
    id: Math.random().toString(36).substr(2, 9),
    name: 'Alice',
    createdAt: new Date().toISOString(),
  };

  expect(user).toMatchSnapshot({
    id: expect.any(String),
    createdAt: expect.any(String),
  });
});

In this case, id and createdAt are verified with expect.any(String), while name is compared exactly against the snapshot.

TypeScript version:

interface UserRecord {
  id: string;
  name: string;
  createdAt: string;
}

test('creates a user with dynamic fields', () => {
  const user: UserRecord = {
    id: Math.random().toString(36).substr(2, 9),
    name: 'Alice',
    createdAt: new Date().toISOString(),
  };

  expect(user).toMatchSnapshot({
    id: expect.any(String),
    createdAt: expect.any(String),
  });
});

You can also use property matchers with inline snapshots:

test('user with dynamic fields inline', () => {
  const user = {
    id: 'abc123',
    name: 'Alice',
    createdAt: '2025-01-01T00:00:00.000Z',
  };

  expect(user).toMatchInlineSnapshot(
    {
      id: expect.any(String),
      createdAt: expect.any(String),
    },
    `
    {
      "createdAt": Any<String>,
      "id": Any<String>,
      "name": "Alice",
    }
  `
  );
});

Custom Serializers

When Jest's default serializer doesn't suit your needs, you can create custom serializers.

// stripAnsi.serializer.js
// Custom serializer that removes ANSI escape codes
module.exports = {
  serialize(val) {
    return val.replace(/\u001b\[[0-9;]*m/g, '');
  },
  test(val) {
    return typeof val === 'string' && /\u001b\[[0-9;]*m/.test(val);
  },
};

Register in Jest Config

// jest.config.js
module.exports = {
  snapshotSerializers: ['./stripAnsi.serializer.js'],
};

Add Per-Test

expect.addSnapshotSerializer({
  serialize(val) {
    // format Date objects as YYYY-MM-DD
    return `"${val.toISOString().split('T')[0]}"`;
  },
  test(val) {
    return val instanceof Date;
  },
});

test('date snapshot', () => {
  const date = new Date('2025-06-15T12:00:00Z');
  expect(date).toMatchInlineSnapshot(`"2025-06-15"`);
});

TypeScript version:

import type { NewPlugin } from 'pretty-format';

const dateSerializer: NewPlugin = {
  serialize(val: Date) {
    return `"${val.toISOString().split('T')[0]}"`;
  },
  test(val: unknown): val is Date {
    return val instanceof Date;
  },
};

expect.addSnapshotSerializer(dateSerializer);

Snapshot Testing for Non-UI Data

Snapshot testing isn't just for UI components. It works well for many types of data structures.

API Response Structure

// apiClient.test.js
const { fetchUserProfile } = require('./apiClient');

test('user profile API response structure', async () => {
  const profile = await fetchUserProfile(1);

  expect(profile).toMatchSnapshot({
    id: expect.any(Number),
    lastLogin: expect.any(String),
  });
});

Config Objects

// config.test.js
const { getConfig } = require('./config');

test('production config snapshot', () => {
  const config = getConfig('production');

  expect(config).toMatchInlineSnapshot(`
    {
      "database": {
        "host": "db.production.example.com",
        "port": 5432,
        "ssl": true,
      },
      "logging": {
        "level": "error",
      },
    }
  `);
});

Error Messages

// validator.test.js
const { validate } = require('./validator');

test('validation error messages', () => {
  const errors = validate({
    name: '',
    email: 'invalid',
    age: -1,
  });

  expect(errors).toMatchInlineSnapshot(`
    [
      "Name is required",
      "Email format is invalid",
      "Age must be a positive number",
    ]
  `);
});

Best Practices

1. Keep Snapshots Small and Focused

// BAD: snapshot of the entire component tree
test('renders page', () => {
  const tree = render(<EntirePage />);
  expect(tree).toMatchSnapshot(); // hundreds of lines
});

// GOOD: snapshot of specific parts
test('renders user card', () => {
  const card = render(<UserCard name="Alice" role="admin" />);
  expect(card).toMatchSnapshot();
});

2. Use Descriptive Test Names

// BAD
test('snapshot 1', () => { ... });

// GOOD
test('renders error state when API fails', () => { ... });

3. Commit Snapshot Files

Include the __snapshots__/ directory in version control. This allows code reviewers to verify snapshot changes.

4. Clean Up Obsolete Snapshots

When you delete tests, their corresponding snapshots remain. Periodically clean them up:

npx jest --ci --updateSnapshot
flowchart TB
    subgraph Good["Good Snapshots"]
        G1["Small and focused"]
        G2["Changes are meaningful"]
        G3["Easy to review"]
    end
    subgraph Bad["Bad Snapshots"]
        B1["Huge and all-encompassing"]
        B2["Changes are unclear"]
        B3["Blindly updated"]
    end
    style Good fill:#22c55e,color:#fff
    style Bad fill:#ef4444,color:#fff

Common Pitfalls

Snapshot Fatigue

When you have too many snapshot tests, developers tend to run jest -u to bulk-update without reviewing diffs. This defeats the purpose of snapshot testing entirely.

flowchart LR
    A["Too many snapshots"] --> B["Many failures\non every change"]
    B --> C["Bulk update\nwithout reviewing"]
    C --> D["Bugs slip through"]
    D --> E["Snapshot tests\nbecome meaningless"]
    style A fill:#f59e0b,color:#fff
    style E fill:#ef4444,color:#fff

Solutions:

  • Only use snapshots where they truly add value
  • Break large snapshots into smaller, focused ones
  • Always review snapshot diffs during code review

Meaningless Updates

Platform-dependent values or auto-generated class names can cause snapshots to break without any meaningful code change.

// BAD: platform-dependent snapshot
test('snapshot with platform-dependent value', () => {
  expect(process.platform).toMatchSnapshot();
});

// BAD: snapshot with CSS-in-JS
test('snapshot with CSS-in-JS', () => {
  // auto-generated class names change on every build
  expect(tree).toMatchSnapshot();
});

Oversized Snapshots

Nobody reviews snapshots that are hundreds of lines long.

Anti-pattern Solution
Snapshot of entire page Split into component-level snapshots
Dynamic values in snapshot Use property matchers
Update without reviewing Review diffs during code review
Snapshot everything Choose the right testing approach

Summary

Concept Description
toMatchSnapshot() Saves value to external file and compares
toMatchInlineSnapshot() Embeds snapshot in test code for comparison
--updateSnapshot / -u Updates snapshots to match current output
Property matchers Handles dynamic values with expect.any()
Custom serializers Customizes snapshot formatting
Snapshot fatigue Problem of ignoring diffs due to too many snapshots

Key Takeaways

  1. Snapshot testing detects "what changed," not "what is correct"
  2. Keep snapshots small and focused
  3. Use property matchers for dynamic values
  4. Always review snapshot diffs during code review
  5. Combine snapshot tests with other testing approaches - don't rely on them exclusively

Exercises

Exercise 1: Basics

Write a snapshot test using toMatchSnapshot() for the following formatProduct function.

function formatProduct(product) {
  return {
    title: product.name.toUpperCase(),
    price: `$${product.price.toFixed(2)}`,
    inStock: product.quantity > 0,
  };
}

Exercise 2: Intermediate

Write a test for the following generateReport function using toMatchInlineSnapshot(), handling the dynamic fields (generatedAt, id) with property matchers.

function generateReport(data) {
  return {
    id: Math.random().toString(36).substr(2, 9),
    title: `Report: ${data.name}`,
    itemCount: data.items.length,
    generatedAt: new Date().toISOString(),
  };
}

Challenge

Create a custom serializer that formats Date objects as YYYY/MM/DD, and write a snapshot test that uses it.


References


Next up: Day 8 covers Coverage and Debugging -- how to measure test coverage, read coverage reports, and debug your tests effectively!