Snapshot testing is immensely popular for testing React apps or other component-based UIs. However, it’s not exactly drama-free — many people looooove snapshots for their ease of use and ability to quickly bootstrap a testing portfolio, while others feel that the long-term effects of snapshots might be more harmful than they are helpful.
At the end of the day, snapshot testing is simply another tool in our tool belt. And while many people may be divided on how and when to use snapshot testing, it’s good to know that it exists and that it’s available when we need it.
I’ll be honest about my position on snapshots — I tend to be in the camp that’s less enthusiastic about them. However, I recently came across a situation with some legacy code where it felt like snapshot tests were a perfect match. Using snapshots as a refactoring tool helped me successfully tackle and refactor some tricky code written long before I joined my company.
If you’re not familiar with snapshot tests, we’ll do a little refresher. In a snapshot test, a “picture” of your code’s output is taken the first time the test runs. This “picture” gets saved to a text file in your codebase and all subsequent test runs use this picture as a reference — if your code’s output produces an identical snapshot, the test passes. However, if the output is different from the saved snapshot the test fails.
Here’s an example of what a snapshot test looks like in Jest:
After this test runs for the first time it will create a snapshot file that future test runs will use as a reference. The snapshot file would look something like this:
At first glance, snapshot testing sounds like a dream come true — all I have to do is write one snippet of code to generate snapshots and I’ll have these super-detailed tests “for free”? Take my money already!
However, over the past few years that I’ve been working with snapshot testing, I’ve found that snapshots introduce a number of pain points that make them difficult to maintain. And I’m not the only one! For example, this company decided to ditch snapshots and wrote about it. Or consider this tweet:
— Justin Searls (@searls) October 15, 2017
That’s not to say snapshot testing is all bad! After all, every tool has trade-offs, and it’s worth acknowledging the weaknesses of a tool when we’re evaluating using it. Here are a few reasons why I’m not the biggest fan of having snapshots in my testing suites.
Snapshots are often used to test component trees or large objects. However, since the snapshot takes a picture of every single detail in the component/object, even the slightest change (like fixing a typo in a CSS class) will fail the snapshot test. As a result, you end up with tests that break even when the code still works. These false negatives create a lot of noise and erode your confidence in your testing suite.
You might be thinking, “Isn’t this a good thing?” After all, being a single keypress away from a passing test suite sounds like a dream come true. However, because the tests are so easy to create/update, what tends to happen is that developers care less about the snapshot tests.
In my experience, developers will often simply press the button to update the snapshots without looking to see what changed or if the code is broken. While it is possible to treat your snapshots with the same importance as your code (and recommended in the Jest docs), it requires a ton of diligence. More often, my experience has been seeing engineers blindly update the snapshots and move on with their day (I’ve done it myself many times in the past 😱).
It’s easy to generate a ton of test coverage using snapshots. If your team has a coverage threshold that all code has to meet, snapshots make hitting your coverage numbers a breeze. However, test coverage alone is not a sufficient metric to use to evaluate the quality of your test suite. While test coverage is a valuable tool for seeing gaps in your testing suite, it doesn’t tell you about things like whether your tests are brittle, whether your code stands up to edge cases, or whether the tests accurately test the business requirements.
While I’m not a fan of having snapshots as “long-term residents” of my testing suites, I’ve actually come across a few use cases where they truly shine. For example, refactoring legacy code.
Rarely do we start a job and get thrown into greenfield projects — we get codebases that have existed for years. And when we do, those projects can quickly go from a blank slate to nightmare codebase if we’re not careful. At some point in your career, you’re going to have to work on “legacy code” that you didn’t write. And many times those codebases don’t have any tests.
When you start adding features to this legacy code, and you’re faced with a dilemma. You might need to refactor the code to fit new business requirements, but you don’t want to run the risk of breaking something. In order to refactor safely, you need some type of tests in place.
The thing is, taking a pause to write tests for legacy code can sometimes feel like a luxury you don’t have. After all, you’ve got deadlines to hit, and you finally figured out where you need to modify this legacy code. If you take too long of a break you might lose that context that you’ve built up!
Snapshots can actually be super useful to us in this scenario. Here’s a snapshot testing workflow I’ve found super helpful when working with legacy code.
Read through the legacy code and try to get a picture of all of the various inputs that it could possibly have. However, you don’t need to figure out the outputs! For each input variant, create a snapshot test. This helps you figure out what outputs are actually produced by the code you’re working with.
Since you’ve got this massive safety net of snapshot tests to fall back on, start refactoring. Remember that this method of refactoring with snapshots is only good if you do not change the output at all. So if you’re working with a React component and you change the rendered output your snapshots will fail. This isn’t the end of the world, just make sure to check why the snapshots failed and if the change was actually intended.
Once you’re done refactoring, you can safely replace these snapshots without fear of forgetting how you wanted to refactor the legacy code. However, for the reasons discussed above, you might not want those snapshots to be long-term residents of your testing suite. Now that the code isn’t changing, you can safely start refactoring your tests. To make your tests more resilient long-term, you might want to consider taking each snapshot test and replacing it with a more focused assertion. For example, we could replace the snapshot test from before with this test using
Granted, this isn’t an incredibly complex test — the component doesn’t have any logic to refactor! These more focused assertions will stand the test of time (pun intended 😂) better as the component changes with future requirements.
Throughout my (short) career I’ve seen lots of code written without tests by people that have long left the company. It’s no secret that tricky, dense, difficult-to-read code has a negative effect on team morale and that, over time, code should be lovingly refactored to fit new requirements.
However, mocking or complaining about tricky legacy code shouldn’t be our default response — instead, we should try to always leave the code in better shape than when we found it.
This is easier said than done, especially when we’re trying to meet a tight deadline or if we’re afraid to touch the code lest we break something. This method of using Jest snapshots has been incredibly useful for me and I hope that you will find it useful too!
Thanks for reading! If you enjoyed this post, make sure to follow me on Twitter — I make sure to post links to any new articles as I write them. If you’ve had some snapshot test success stories, don’t hesitate to reach out!
Install LogRocket via npm or script tag.
LogRocket.init() must be called client-side, not
Web components are underrated for the performance and ergonomic benefits they provide in vanilla JS. Learn how to nest them in this post.
defer feature, introduced in Angular 17, can help us optimize the delivery of our apps to end users.
ElectricSQL is a cool piece of software with immense potential. It gives developers the ability to build a true local-first application.
Leptos is an amazing Rust web frontend framework that makes it easier to build scalable, performant apps with beautiful, declarative UIs.