Snapshot Testing in React

Loading

Snapshot testing is a powerful technique for verifying that your UI components don’t change unexpectedly. Here’s how to implement it effectively in React applications:

1. Basic Snapshot Testing with Jest

Setup (already included in Create React App)

npm install --save-dev jest react-test-renderer @types/jest

First Snapshot Test

// Button.js
import React from 'react';

const Button = ({ children }) => (
  <button className="primary-button">{children}</button>
);

export default Button;

// Button.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Button from './Button';

test('Button renders correctly', () => {
  const tree = renderer
    .create(<Button>Click me</Button>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

When you run this test (npm test), Jest creates a __snapshots__ directory with a snapshot file.

2. Updating Snapshots

When you intentionally change a component:

npm test -- -u  # Updates all snapshots
# or for specific tests
npm test -- Button -u

3. Testing Different States

// Button.test.js
test('Button renders correctly in all states', () => {
  const primary = renderer.create(<Button primary>Primary</Button>).toJSON();
  const secondary = renderer.create(<Button>Secondary</Button>).toJSON();
  const disabled = renderer.create(<Button disabled>Disabled</Button>).toJSON();

  expect(primary).toMatchSnapshot();
  expect(secondary).toMatchSnapshot();
  expect(disabled).toMatchSnapshot();
});

4. Snapshot Testing with React Testing Library

import { render } from '@testing-library/react';
import Button from './Button';

test('renders correctly with Testing Library', () => {
  const { container } = render(<Button>Test</Button>);
  expect(container.firstChild).toMatchSnapshot();
});

5. Testing Connected Components

Redux Connected Components

import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';

test('Connected component snapshot', () => {
  const mockStore = configureStore([]);
  const store = mockStore({ user: { name: 'John' } });

  const tree = renderer
    .create(
      <Provider store={store}>
        <ConnectedUserProfile />
      </Provider>
    )
    .toJSON();

  expect(tree).toMatchSnapshot();
});

React Router Components

import { MemoryRouter } from 'react-router-dom';

test('Route component snapshot', () => {
  const tree = renderer
    .create(
      <MemoryRouter initialEntries={['/users/123']}>
        <App />
      </MemoryRouter>
    )
    .toJSON();

  expect(tree).toMatchSnapshot();
});

6. Serializers for Better Snapshots

Custom Serializer (simplifying output)

// In your Jest config or setupTests.js
expect.addSnapshotSerializer({
  test: (val) => val && val.props && val.props.className,
  print: (val) => {
    const props = {};
    Object.keys(val.props).forEach(key => {
      if (key !== 'children') {
        props[key] = val.props[key];
      }
    });

    return `ReactElement: ${val.type} ${JSON.stringify(props)}`;
  }
});

7. Best Practices

  1. Meaningful Snapshots:
  • Keep snapshots small and focused
  • Test one component at a time
  • Avoid large DOM trees
  1. Review Changes:
  • Always review snapshot diffs
  • Don’t blindly accept all changes
  1. Deterministic Tests:
  • Mock dates, timers, and random values
   beforeAll(() => {
     jest.useFakeTimers('modern');
     jest.setSystemTime(new Date(2023, 0, 1));
   });
  1. Avoid Snapshots For:
  • Error boundaries
  • Components with random outputs
  • Frequently changing components
  1. CI Integration:
   # .github/workflows/test.yml
   - name: Run tests
     run: npm test -- --ci --watchAll=false

8. Advanced Techniques

Inline Snapshots

test('inline snapshot example', () => {
  const result = renderer.create(<Button>OK</Button>).toJSON();
  expect(result).toMatchInlineSnapshot(`
    <button
      className="primary-button"
    >
      OK
    </button>
  `);
});

Property Matchers

test('matches with property matchers', () => {
  const user = {
    id: expect.any(Number),
    name: 'John',
    createdAt: expect.any(Date)
  };

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

Snapshot Testing Hooks

function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(c => c + 1);
  return { count, increment };
}

test('hook snapshot', () => {
  const result = {};
  function TestComponent() {
    result.current = useCounter();
    return null;
  }

  render(<TestComponent />);
  expect(result.current).toMatchSnapshot();
});

9. Troubleshooting

Fixing Flaky Snapshots

// Before snapshot
test('flaky date snapshot', () => {
  const component = renderer.create(<DateDisplay />);

  // Mock the date
  const mockDate = new Date(2023, 0, 1);
  jest.spyOn(global, 'Date').mockImplementation(() => mockDate);

  expect(component.toJSON()).toMatchSnapshot();

  // Cleanup
  jest.restoreAllMocks();
});

Ignoring Dynamic Values

test('ignores dynamic class names', () => {
  const tree = renderer.create(<DynamicComponent />).toJSON();

  expect(tree).toMatchSnapshot({
    props: {
      className: expect.stringMatching(/button-\d+/)
    }
  });
});

Snapshot testing is most effective when combined with other testing methods. Use it as part of a comprehensive testing strategy that includes unit tests and integration tests for complete coverage.

Leave a Reply

Your email address will not be published. Required fields are marked *