Skip to main content

Snapshot Testing in Vitest: Best Practices

A snapshot test captures the output of a component (or function) and saves it as a reference. Future runs compare new output to the saved snapshot; if they differ, the test fails. Snapshot tests are controversial—they're useful for detecting unintended changes but easy to misuse (developers update snapshots without reviewing changes). This article teaches you when snapshots help, when they hurt, and how to use them responsibly (Vitest documentation, 2026).

When to Use Snapshots (and When Not To)

Use snapshots for:

  • Large, complex component output that's hard to assert line-by-line.
  • Rendered HTML that changes rarely (stable components).
  • Comparing deeply nested objects (API responses, config files).

Don't use snapshots for:

  • User-facing text (test the text instead—snapshots don't catch typos).
  • Small components (write specific assertions instead).
  • Frequently-changing components (snapshots become maintenance burden).
  • Dynamic content (timestamps, IDs, random data).

Example: DON'T snapshot this:

// ❌ Bad snapshot test
it('renders welcome message', () => {
render(<WelcomeCard name="Alice" />);
expect(screen.getByRole('heading')).toMatchSnapshot(); // Brittle
});

Example: DO use specific assertions:

// ✓ Good: specific assertion
it('renders welcome message', () => {
render(<WelcomeCard name="Alice" />);
expect(screen.getByRole('heading')).toHaveTextContent('Welcome, Alice!');
});

Example: Good snapshot test:

// ✓ Good snapshot: rarely-changed, complex output
it('renders form with all fields', () => {
const { container } = render(<ComplexForm />);
expect(container).toMatchSnapshot();
});

Basic Snapshot Syntax

Create a snapshot test with toMatchSnapshot():

import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { ErrorMessage } from './ErrorMessage';

describe('ErrorMessage', () => {
it('renders error state', () => {
const { container } = render(
<ErrorMessage error="Payment failed" code="PAYMENT_DECLINED" />
);

expect(container).toMatchSnapshot();
});
});

Run tests:

npm test -- --reporter=verbose

First run: Vitest creates __snapshots__/ErrorMessage.test.tsx.snap:

// __snapshots__/ErrorMessage.test.tsx.snap
exports[`ErrorMessage › renders error state 1`] = `
<div>
<div class="error-container">
<h2 class="error-title">Error</h2>
<p class="error-message">Payment failed</p>
<p class="error-code">PAYMENT_DECLINED</p>
</div>
</div>
`;

Subsequent runs compare new output to this snapshot. If the component changes, tests fail and you must review and update the snapshot.

Updating Snapshots

When component output changes intentionally, update the snapshot:

npm test -- --update

Or interactively:

npm test -- --reporter=verbose
# When a snapshot fails, press 'u' to update

Important: Always review changes before updating. Snapshots hide bugs—a typo in component markup gets saved as the "correct" snapshot if you're not paying attention.

Inline Snapshots

For smaller snapshots, embed them directly in test code:

it('formats date correctly', () => {
expect(formatDate(new Date('2026-06-02'))).toMatchInlineSnapshot(`
"June 2, 2026"
`);
});

When this test runs next, if the output changes to "2026-06-02", the test fails and you update the inline snapshot:

// After running `npm test -- --update`
expect(formatDate(new Date('2026-06-02'))).toMatchInlineSnapshot(`
"2026-06-02"
`);

Inline snapshots are better for small outputs because changes are obvious when you read the diff.

Handling Dynamic Content in Snapshots

Some data changes every run (timestamps, IDs, random values). Don't snapshot these directly—filter them:

it('creates user with dynamic ID', () => {
const user = {
id: 'uuid-12345', // ❌ Different every run
name: 'Alice',
createdAt: '2026-06-02T12:00:00Z', // ❌ Timestamp changes
};

// Don't snapshot the ID or timestamp directly
const { id, createdAt, ...rest } = user;

expect(rest).toMatchSnapshot(); // Only snapshot name
expect(id).toMatch(/^uuid-/); // Assert ID format separately
});

Or use snapshot matchers to ignore dynamic values:

expect(user).toMatchSnapshot({
id: expect.any(String), // Allow any string ID
createdAt: expect.any(String), // Allow any timestamp
});

Snapshot Testing with DOM Content

Test rendered HTML:

it('renders form snapshot', () => {
const { container } = render(<LoginForm />);
expect(container.firstChild).toMatchSnapshot();
});

The snapshot captures the full DOM structure. If you add a field, change a label, or modify styling, the snapshot fails and you review the change.

Common Snapshot Pitfalls

Committing wrong snapshots: Always review diffs. Use git diff __snapshots__/ before committing:

git diff __snapshots__/

If changes look wrong, revert and fix the component, not the snapshot.

Snapshots becoming stale: When refactoring, snapshots break and developers just update them without thinking. Use snapshots sparingly to avoid this.

Snapshot files getting huge: Snapshots for large components can become difficult to review. If a snapshot exceeds 50 lines, consider breaking the component into smaller snapshots or using specific assertions instead.

False confidence: A passing snapshot doesn't mean the component looks good—it only means the output hasn't changed. The original snapshot might be wrong. Always review snapshots visually before committing.

Snapshot Testing Best Practices

1. Review every snapshot diff:

git diff __snapshots__/

2. Snapshot rarely-changed, complex components:

// ✓ Good: complex form with many fields
expect(<Form />).toMatchSnapshot();

// ❌ Bad: simple button
expect(<Button />).toMatchSnapshot();

3. Use inline snapshots for small outputs:

// ✓ Good: small value, easy to review inline
expect(formatDate(date)).toMatchInlineSnapshot(`"June 2, 2026"`);

// ❌ Bad: large output inline is hard to read
expect(largeObject).toMatchInlineSnapshot(`...long output...`);

4. Filter dynamic data:

// ✓ Good: exclude dynamic values
const { id, createdAt, ...rest } = data;
expect(rest).toMatchSnapshot();

// ❌ Bad: snapshots change every run
expect(data).toMatchSnapshot();

5. Pair snapshots with specific assertions:

it('renders success message', () => {
render(<Success message="Payment processed" />);

// Snapshot for structure
expect(screen.getByRole('alert')).toMatchSnapshot();

// Specific assertion for content
expect(screen.getByText('Payment processed')).toBeInTheDocument();
});

Real Example: Complex Form Snapshot

describe('PaymentForm', () => {
it('renders with all fields and validation state', () => {
const { container } = render(
<PaymentForm
methods={['card', 'paypal']}
defaultMethod="card"
errors={{ cardNumber: 'Invalid' }}
/>
);

expect(container.firstChild).toMatchSnapshot();
});
});

Snapshot captures:

  • Form structure (fields, labels, buttons).
  • Error messages and styling.
  • Conditional rendering (based on defaultMethod, errors).

When the form markup changes, the snapshot fails. You review the diff—if changes are intended, update with npm test -- --update. If changes are wrong, fix the component.

Key Takeaways

  • Use snapshots for complex, rarely-changed components; avoid them for simple output or frequently-changing components.
  • Always review snapshot diffs with git diff __snapshots__/ before committing.
  • Filter dynamic data (IDs, timestamps) so snapshots are stable.
  • Use inline snapshots for small outputs; file snapshots for large ones.
  • Snapshots don't guarantee correctness—they only catch unintended changes.
  • Pair snapshots with specific assertions for important content.

Frequently Asked Questions

When should I snapshot a component vs. assert specific content?

Snapshot when the component structure is complex and stable (rarely changes). Assert specific content when testing user-facing text or critical functionality. For example:

// Snapshot: complex form structure
expect(form).toMatchSnapshot();

// Assertion: important message
expect(screen.getByText('Payment successful')).toBeInTheDocument();

How do I prevent developers from mindlessly updating snapshots?

Use a pre-commit hook that requires snapshot diffs to be reviewed:

npm install -D husky lint-staged
npx husky install

In .husky/pre-commit:

git diff __snapshots__/ && echo "Review snapshot changes!"

Or enforce snapshot review in your CI/CD (require approval for snapshot changes).

Can I use snapshots for visual testing (does the component look right)?

No. Snapshots verify structure, not appearance. For visual testing, use visual regression tools like Chromatic or Percy (external services). Snapshots are for catching accidental markup changes.

What if a snapshot becomes too large to review?

Break the component into smaller snapshots:

it('renders header', () => {
expect(screen.getByRole('banner')).toMatchSnapshot();
});

it('renders main content', () => {
expect(screen.getByRole('main')).toMatchSnapshot();
});

it('renders footer', () => {
expect(screen.getByRole('contentinfo')).toMatchSnapshot();
});

How do I snapshot third-party component props?

Snapshot the props object, not the rendered output:

it('passes correct props to Dialog', () => {
const { result } = renderHook(() => useDialog());

expect(result.current.dialogProps).toMatchSnapshot();
});

This way, if you accidentally change a prop, the test catches it.

Further Reading