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
- Meaningful Snapshots:
- Keep snapshots small and focused
- Test one component at a time
- Avoid large DOM trees
- Review Changes:
- Always review snapshot diffs
- Don’t blindly accept all changes
- Deterministic Tests:
- Mock dates, timers, and random values
beforeAll(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date(2023, 0, 1));
});
- Avoid Snapshots For:
- Error boundaries
- Components with random outputs
- Frequently changing components
- 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.