Introduction
I came across this question in one of the unit testing discussions so thought of sharing it with everyone else. I will not be covering what is snapshot testing in this article. Please check this link for more info about snapshot testing.
Well, snapshot testing is a good way to capture the state of the DOM (Document Object Model). It will help you identify if your code has made any DOM-level changes.
Let's take an example by creating a Todo App -
App.tsx
import React, { useState } from 'react';
import './style.css';
const App = () => {
const [todos, setTodos] = useState<Array<string>>([]);
const [todoInput, setTodoInputValue] = useState('');
const addTodoItem = () => {
if (todoInput) {
setTodos([...todos, todoInput]);
setTodoInputValue('');
}
};
const clearTodoList = () => {
setTodos([]);
};
return (
<div>
<h2>Todo List</h2>
<ul className="todo-items">
{todos.map((todo) => (
<li>{todo}</li>
))}
</ul>
<textarea
data-testid="input"
value={todoInput}
onChange={(e) => setTodoInputValue(e.target.value)}
/>
<div className="buttons-wrapper">
<button type="reset" onClick={clearTodoList}>
Clear
</button>
<button onClick={addTodoItem}>Add</button>
</div>
</div>
);
}
export default App;
Adding Snapshot Test
Let's add some unit tests to test for this component
App.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
describe('<App/>', () => {
it('should match the snapshot', () => {
const container = render(<App />);
expect(container.container).toMatchSnapshot();
});
});
This will generate the following snapshot in __snapshots__ directory -
// Jest Snapshot v1
exports[`<App/> should match the snapshot 1`] = `
<div>
<div>
<h2>
Todo List
</h2>
<ul
class="todo-items"
/>
<textarea />
<div
class="buttons-wrapper"
>
<button
type="reset"
>
Clear
</button>
<button>
Add
</button>
</div>
</div>
</div>
`;
the above snapshot contains all the DOM elements along with the labels and element attributes. So this is a good way to test the structure or hierarchy of the DOM. but, it will not capture the events and any other methods in your component.
how do we capture methods and events in the snapshot tests?
Although we cannot capture the code of the methods in the snapshot, we can capture the DOM changes which were caused by the execution of methods or events.
let's add one more snapshot to test the add method -
App.test.tsx
it('should match the snapshot when a todo item is added', () => {
const renderResult = render(<App />);
const inputElement = renderResult.getByTestId('input');
const addButton = renderResult.getByText('Add');
fireEvent.change(inputElement, { target: { value : "Todo Item 1"}});
fireEvent.click(addButton);
expect(renderResult.container).toMatchSnapshot();
});
This will generate the following snapshot -
exports[`<App/> should match the snapshot when a todo item is added 1`] = `
<div>
<div>
<h2>
Todo List
</h2>
<ul
class="todo-items"
>
<li>
Todo Item 1
</li>
</ul>
<textarea
data-testid="input"
/>
<div
class="buttons-wrapper"
>
<button
type="reset"
>
Clear
</button>
<button>
Add
</button>
</div>
</div>
</div>
`;
Now you can see the new Todo Item 1 added via a unit test in the snapshot. This tested addTodoItem method.
This is how you can test some of the internal workings of the component but at the cost of additional snapshots and this will increase with the variations of tests you want to add for e.g. Unit test to check if the clear button clears the to-do Items.
What can't be tested using snapshots?
But, there are a couple of things you may still not be able to test -
Functionalities which does not lead to DOM changes cannot be tested since they won't be captured in the snapshot.
Functionalities which change DOM elements in the parent component (or any other component which is not a part of the component under test) cannot be tested since they won't be captured in the snapshot. The workaround for this would be to write the test in the parent.
The easiest way to test this Todo App component would be to add a snapshot for the initial state of the component and other functionality can be tested using simple unit tests -
App.test.tsx
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import App from './App';
describe('<App/>', () => {
// snapshot test for capturing the initial state
it('should match the snapshot', () => {
const renderResult = render(<App />);
expect(renderResult.container).toMatchSnapshot();
});
// other unit tests not using snapshot testing
it('should add a todo item when clicked on add', () => {
const renderResult = render(<App />);
const inputElement = renderResult.getByTestId('input');
const addButton = renderResult.getByText('Add');
fireEvent.change(inputElement, { target: { value : "Todo Item 1"}});
fireEvent.click(addButton);
expect(renderResult.getByText("Todo Item 1"));
});
it('should clear all todos when clicked on clear', () => {
const renderResult = render(<App />);
const inputElement = renderResult.getByTestId('input');
const addButton = renderResult.getByText('Add');
const clearButton = renderResult.getByText('Clear');
fireEvent.change(inputElement, { target: { value: "Todo Item 1" } });
fireEvent.click(addButton);
fireEvent.click(clearButton)
expect(renderResult.queryByText("Todo Item 1")).toBeEmptyDOMElement;
});
});
Summary
Now let's summarise the downsides of going ONLY for snapshot testing -
Events & handlers methods code is not captured until they result in DOM changes in the same component.
Additional files or snapshots are added to the repo if multiple variations of the test are needed.
Merge conflicts will be a headache since every time we get a conflict we will have to resolve the merge conflict first, then regenerate the snapshot via tests, and then add it to the commit. so waste of time and energy.
Code coverage if we are heavily relying on the snapshot tests - the code coverage may be good but it will not give enough confidence since internal workings on the component are not captured in the snapshot.
Failures in snapshots are not paid much attention by the devs and they tend to directly update which is the biggest downside. additionally, it is difficult for the devs to figure out what went wrong.
External Library updates - can cause the snapshots to change which will result in test failures and it will get difficult for the devs to identify the fix.
Increased number of Test Reruns - Especially to update the snapshots.
because of all the above reasons, I feel you should only go for snapshot tests when the code results in a DOM change otherwise prefer normal unit tests. Additionally, In big projects, it is better not to have snapshot tests at all.
Feel free to share your thoughts in the comment section below.
Thanks for reading!