When Snapshot Testing will make you snap

, Kriese Dominik

In modern frontend development, there is a rise of component based frameworks and libraries such as React and Angular. And with them came new ways of testing Web-UI-Code. The most well known example is snapshot testing. It grew within the React community and offers a lot of benefits.

However, today we want to focus on the things that can go wrong. So let us first have a brief look at what the method is. Afterwards we will see how it could undermine your testing efforts.

What is snapshot testing

Snapshot testing is a term used to describe approval testing for the frontend community. Or is it the other way round? Anyways: The basic idea is to take pieces of plain text and compare them to a stored baseline.

Frontend developers love this technique. And for good reason. With little to no effort, we can write tests, that do a multitude of things in regards to verifying our DOM and data structures. A snapshot test could look like this, with the resulting snapshot below.

it('should style a button', () => {
    const callback = jest.fn();
    const button = mount(<Button label={'Button'} clickHandler={callback}/>)
        .find('button');
    expect(button).toMatchSnapshot();
});
exports[`Button should style a button 1`] = `
.c0 {
  background-color: #0f7956;
  border: none;
  border-radius: 4px;
  padding: 6px 24px;
  color: white;
  font-weight: bold;
  float: right;
  margin-top: 16px;
}

.c0:hover {
  background-color: #21ae80;
  cursor: pointer;
}

<button
  className="c0"
  onClick={[MockFunction]}
>
  Button
</button>
`;

One thing stands out: very little lines of test code validate both, our CSS and HTML code. That looks awesome and saves time. But be aware of the hidden traps.

Snapshot Testing can break your teams discipline

What happens a lot, when developers get excited for snapshot testing is clear. They over use it. But how can this happen? The answer is simple. Lets asume you have a component or page like the following and use a snapshot test to verify it.

const Application = styled.div`
  background-color: #f3f3f3;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #333;
  text-align: center;
`

const App = () => {

    const defaultText = "Enter some text";
    let initialState = localStorage.load();
    const [label, setLabel] = useState(initialState ?? defaultText);
    const [input, setInput] = useState('');


    const updateLabel = () => {
        setLabel(!!input ? input : defaultText);
        setInput("");
        localStorage.save(input);
    }

    const inputLabel = 'Enter your text';
    return (
        <Application data-testid={'application'}>
            <Container>
                <Headline text={label}/>
                <TextInput value={input}
                           changeHandler={setInput}
                           label={inputLabel}
                           id={'text'}/>
                <Button label={'UPDATE'} clickHandler={updateLabel}/>
            </Container>
        </Application>
    );
}

export default App;

As you might assume, the used components have some code on their own. A snapshot of this will result in a wall of text. This leads to three main problems. First, the test will fail with every change in our application. Second, it will never fail alone, since we hopefully test our components as well. And third: think about a project deadline. Would you really read the following snapshot?

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Application should never be tested as a snapshot 1`] = `
.c4 {
  display: block;
  padding: 6px 12px;
  border-bottom: 1px solid #333;
  border-top: none;
  border-right: none;
  border-left: none;
  background-color: #f3f3f3;
  font-size: 1.25rem;
  width: 100%;
  box-sizing: border-box;
}

.c4:focus {
  border-bottom: 2px solid #0f7956;
  padding-bottom: 5px;
}

.c3 {
  display: block;
  margin-top: 16px;
  font-size: 0.75rem;
  background-color: #f3f3f3;
  padding: 4px 12px 0;
  width: 100%;
  text-align: left;
  box-sizing: border-box;
}

.c2 {
  text-align: left;
  font-size: 2.5rem;
}

.c5 {
  background-color: #0f7956;
  border: none;
  border-radius: 4px;
  padding: 6px 24px;
  color: white;
  font-weight: bold;
  float: right;
  margin-top: 16px;
}

.c5:hover {
  background-color: #21ae80;
  cursor: pointer;
}

.c1 {
  background-color: #eaeaea;
  border-radius: 4px;
  padding: 32px;
  -webkit-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
  -moz-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
  box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
}

.c0 {
  background-color: #f3f3f3;
  min-height: 100vh;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-flex-direction: column;
  -ms-flex-direction: column;
  flex-direction: column;
  -webkit-align-items: center;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  color: #333;
  text-align: center;
}

<App>
  <styled.div
    data-testid="application"
  >
    <div
      className="c0"
      data-testid="application"
    >
      <Container>
        <styled.div>
          <div
            className="c1"
          >
            <Headline
              text="Enter some text"
            >
              <styled.h1>
                <h1
                  className="c2"
                >
                  Enter some text
                </h1>
              </styled.h1>
            </Headline>
            <TextInput
              changeHandler={[Function]}
              id="text"
              label="Enter your text"
              value=""
            >
              <styled.label
                htmlFor="text"
              >
                <label
                  className="c3"
                  htmlFor="text"
                >
                  Enter your text
                </label>
              </styled.label>
              <styled.input
                id="text"
                onChange={[Function]}
                type="text"
                value=""
              >
                <input
                  className="c4"
                  id="text"
                  onChange={[Function]}
                  type="text"
                  value=""
                />
              </styled.input>
            </TextInput>
            <Button
              clickHandler={[Function]}
              label="UPDATE"
            >
              <styled.button
                onClick={[Function]}
              >
                <button
                  className="c5"
                  onClick={[Function]}
                >
                  UPDATE
                </button>
              </styled.button>
            </Button>
          </div>
        </styled.div>
      </Container>
    </div>
  </styled.div>
</App>
`;

Did you notice the buttons color? I didn’t. Most developers I know would be confident enough to trust their capabilities. They would enter jest -u and “solve” the problem at hand. And thats not even 200 lines for our small sample application. Imagine real world pages or templates, rendering up to thousands of lines.

While the first two points are just annoying, but not a big deal, the last is bad enough to cause serious headaches. At this point, the usefulness of your snapshot tests relies completely on the discipline of your team! As did the value of all of your tests, since those snapshots make it hard to find real problems. And it will happen, that someone updates the snapshots without even realizing, that there is a problem within another component.

Is Snapshot Testing bad?

No! Snapshot Testing is awesome. I love it. The moment i discovered it, I started to save insane amounts of time. All I am saying is: Use the right tool for the job. If you combine multiple small or medium sized components to a template or a page, snapshot testing is often the wrong choice. You could either use visual regression for that purpose or just check if all your components are displayed individually.

Are you looking at a small component, leading to a snapshot with less than 100 lines? Use it. It is awesome! And never forget: It is more than just a tool for validating DOM integrity. Do you have an algorithm producing data structures? Snapshot testing can be useful as well. But always remember: If you can not read the snapshot within half a minute, consider using something else.

Some final words

Personally, I am done talking about snapshot testing for today. But if you want to learn more about testing for your Frontend Code, stay with us. We will cover several other topics in the near future. And one important thing before I am done: leave a comment. I’d love to learn from your experience!

General inquiries

We look forward to tackling your challenges together and discussing suitable solutions. Contact us - and get tailored solutions for your business. We look forward to your contact request!

Contact Us