Snapshot Testing Pattern
Use snapshot testing effectively for UI components, data structures, and generated output with Vitest inline and file snapshots.
Overview#
Snapshot testing captures the output of code and compares it against a stored reference. It's useful for detecting unintended changes to UI components and data transformations.
When to use:
- Testing component render output
- Verifying serialized data structures
- Testing generated code or configurations
- Catching unintended UI changes
Key features:
- Automatic snapshot creation
- Inline and file-based snapshots
- Property matchers for dynamic values
- Easy snapshot updates
Code Example#
Basic Snapshot#
1// tests/components/Button.test.tsx
2import { render } from '@testing-library/react'
3import { expect, it } from 'vitest'
4import { Button } from '@/components/ui/Button'
5
6it('renders correctly', () => {
7 const { container } = render(<Button>Click me</Button>)
8 expect(container).toMatchSnapshot()
9})
10
11it('renders loading state', () => {
12 const { container } = render(<Button loading>Submit</Button>)
13 expect(container).toMatchSnapshot()
14})
15
16it('renders disabled state', () => {
17 const { container } = render(<Button disabled>Disabled</Button>)
18 expect(container).toMatchSnapshot()
19})Inline Snapshots#
1// More readable for small outputs
2import { expect, it } from 'vitest'
3import { formatCurrency, formatDate, getDisplayName } from '@/lib/utils'
4
5it('formats currency', () => {
6 expect(formatCurrency(1234.56)).toMatchInlineSnapshot(`"$1,234.56"`)
7})
8
9it('formats date', () => {
10 expect(formatDate(new Date('2024-01-15'))).toMatchInlineSnapshot(
11 `"January 15, 2024"`
12 )
13})
14
15it('generates user display name', () => {
16 expect(getDisplayName({ firstName: 'John', lastName: 'Doe' }))
17 .toMatchInlineSnapshot(`"John Doe"`)
18})Object Snapshots#
1// Snapshot complex objects
2import { expect, it } from 'vitest'
3import { transformResponse, buildConfig } from '@/lib/api'
4
5it('transforms API response', () => {
6 const response = {
7 id: '123',
8 created_at: '2024-01-01T00:00:00Z',
9 user_name: 'johndoe',
10 is_active: true
11 }
12
13 expect(transformResponse(response)).toMatchInlineSnapshot(`
14 {
15 "id": "123",
16 "createdAt": "2024-01-01T00:00:00.000Z",
17 "userName": "johndoe",
18 "isActive": true
19 }
20 `)
21})
22
23it('builds configuration object', () => {
24 const config = buildConfig({
25 name: 'my-app',
26 features: ['auth', 'api']
27 })
28
29 expect(config).toMatchInlineSnapshot(`
30 {
31 "name": "my-app",
32 "features": [
33 "auth",
34 "api"
35 ],
36 "version": "1.0.0",
37 "debug": false
38 }
39 `)
40})Property Matchers#
1// Ignore dynamic values
2import { expect, it } from 'vitest'
3
4it('creates user with generated id', async () => {
5 const user = await createUser({ email: 'test@example.com' })
6
7 expect(user).toMatchSnapshot({
8 id: expect.any(String),
9 createdAt: expect.any(Date)
10 })
11})
12
13it('generates order with timestamp', () => {
14 const order = createOrder({ items: [{ id: '1', quantity: 2 }] })
15
16 expect(order).toMatchSnapshot({
17 id: expect.stringMatching(/^order_/),
18 createdAt: expect.any(Date),
19 updatedAt: expect.any(Date),
20 items: [
21 {
22 id: '1',
23 quantity: 2,
24 addedAt: expect.any(Date)
25 }
26 ]
27 })
28})
29
30// Snapshot with asymmetric matchers
31it('returns paginated response', async () => {
32 const response = await fetchUsers({ page: 1, limit: 10 })
33
34 expect(response).toMatchSnapshot({
35 data: expect.arrayContaining([
36 expect.objectContaining({
37 id: expect.any(String),
38 email: expect.stringMatching(/@/),
39 createdAt: expect.any(String)
40 })
41 ]),
42 pagination: {
43 page: 1,
44 limit: 10,
45 total: expect.any(Number),
46 totalPages: expect.any(Number)
47 }
48 })
49})File Snapshots#
1// Large output to separate files
2import { expect, it } from 'vitest'
3
4it('generates config file', () => {
5 const config = generateConfig({
6 name: 'my-app',
7 features: ['auth', 'api', 'db']
8 })
9
10 expect(config).toMatchFileSnapshot('./snapshots/config.json')
11})
12
13it('generates schema', () => {
14 const schema = generateSchema(models)
15 expect(schema).toMatchFileSnapshot('./snapshots/schema.graphql')
16})Custom Serializers#
1// vitest.config.ts
2export default defineConfig({
3 test: {
4 snapshotSerializers: ['./tests/serializers/date.ts']
5 }
6})
7
8// tests/serializers/date.ts
9export const serialize = (val: Date) => `Date<${val.toISOString()}>`
10export const test = (val: unknown) => val instanceof Date
11
12// Output in snapshots:
13// Date<2024-01-01T00:00:00.000Z>
14
15// Custom component serializer
16// tests/serializers/component.ts
17export const serialize = (val: React.ReactElement) => {
18 // Custom serialization logic
19 return prettyPrint(val)
20}
21
22export const test = (val: unknown) => {
23 return val && typeof val === 'object' && '$$typeof' in val
24}Component Variant Snapshots#
1// Snapshot multiple variants
2import { render } from '@testing-library/react'
3import { describe, it, expect } from 'vitest'
4import { Button } from '@/components/ui/Button'
5
6describe('Button', () => {
7 const variants = ['primary', 'secondary', 'danger', 'ghost'] as const
8 const sizes = ['sm', 'md', 'lg'] as const
9
10 // Test all variant and size combinations
11 variants.forEach(variant => {
12 sizes.forEach(size => {
13 it(`renders ${variant} ${size}`, () => {
14 const { container } = render(
15 <Button variant={variant} size={size}>
16 Button
17 </Button>
18 )
19 expect(container.firstChild).toMatchSnapshot()
20 })
21 })
22 })
23
24 // Test with different states
25 const states = [
26 { disabled: true },
27 { loading: true },
28 { disabled: false, loading: false }
29 ]
30
31 states.forEach(state => {
32 it(`renders with state ${JSON.stringify(state)}`, () => {
33 const { container } = render(
34 <Button {...state}>Button</Button>
35 )
36 expect(container.firstChild).toMatchSnapshot()
37 })
38 })
39})Update Snapshots#
1# Update all snapshots
2npm test -- --update
3
4# Update specific test file snapshots
5npm test -- Button.test.tsx --update
6
7# Interactive update (choose which to update)
8npm test -- --watch
9# Then press 'u' to update
10
11# Update only failing snapshots
12npm test -- --update --reporter=verboseSnapshot Best Practices Examples#
1// DON'T: Snapshot too much
2it('renders page', () => {
3 const { container } = render(<ComplexPage />)
4 expect(container).toMatchSnapshot() // Too large, hard to review
5})
6
7// DO: Snapshot specific elements
8it('renders header correctly', () => {
9 render(<ComplexPage />)
10 expect(screen.getByRole('banner')).toMatchSnapshot()
11})
12
13// DO: Use inline snapshots for small outputs
14it('formats date', () => {
15 expect(formatDate(new Date('2024-01-15')))
16 .toMatchInlineSnapshot(`"January 15, 2024"`)
17})
18
19// DON'T: Include dynamic data
20it('renders user', () => {
21 const { container } = render(<User id={Math.random()} />)
22 expect(container).toMatchSnapshot() // Will always fail
23})
24
25// DO: Use stable test data
26it('renders user', () => {
27 const { container } = render(
28 <User
29 id="test-id"
30 createdAt={new Date('2024-01-01')}
31 />
32 )
33 expect(container).toMatchSnapshot()
34})
35
36// DON'T: Snapshot implementation details
37it('has correct state', () => {
38 const { result } = renderHook(() => useCounter())
39 expect(result.current).toMatchSnapshot() // Internal state
40})
41
42// DO: Snapshot observable behavior
43it('renders correct count', () => {
44 render(<Counter initialCount={5} />)
45 expect(screen.getByRole('spinbutton')).toMatchSnapshot()
46})Snapshot Testing Strategy#
1// Use snapshots for UI regression
2describe('Card component', () => {
3 it('matches snapshot', () => {
4 const { container } = render(
5 <Card
6 title="Test Card"
7 description="Test description"
8 image="/test.jpg"
9 />
10 )
11 expect(container).toMatchSnapshot()
12 })
13})
14
15// Use regular assertions for behavior
16describe('Card component behavior', () => {
17 it('calls onClick when clicked', () => {
18 const onClick = vi.fn()
19 render(<Card onClick={onClick} />)
20
21 fireEvent.click(screen.getByRole('article'))
22 expect(onClick).toHaveBeenCalled()
23 })
24
25 it('renders image with alt text', () => {
26 render(<Card image="/test.jpg" imageAlt="Test image" />)
27 expect(screen.getByAltText('Test image')).toBeInTheDocument()
28 })
29})Usage Instructions#
- Write test with toMatchSnapshot(): Vitest creates snapshot on first run
- Review snapshot files: Verify the captured output is correct
- Commit snapshots: Include snapshot files in version control
- Update when needed: Run with
--updateflag after intentional changes - Review in PRs: Check snapshot changes carefully during code review
Best Practices#
- Keep snapshots small - Snapshot specific elements, not entire pages
- Use inline for small outputs - More readable in the test file
- Use property matchers - Handle dynamic values like IDs and dates
- Review snapshot changes - Don't blindly update failing snapshots
- Complement with assertions - Use snapshots alongside behavior tests
- Avoid snapshots for logic - Test business logic with assertions
- Commit snapshots - They're part of your test suite
Related Patterns#
- Vitest - Test runner setup
- Component Testing - Component testing
- Unit Testing - Unit testing patterns
- Coverage - Code coverage setup