💀Don't Break UI with Jest Snapshot Testing 📷


A few days ago, I was fixing a small accessibility issue and decided to remove some unnecessary wrappers around a few elements. After making those changes, all the tests passed successfully—including the E2E tests. Everything was green, so I opened a pull request, and it got approved. But after merging into develop
, my friend Aitor found out that I had broken the UI 🥲.
But why did this happen?
I had tests to check my code and was sure the data rendered correctly. However, I overlooked child components and UI states like :hover
or :focus
, which rely on the DOM structure and CSS rules.
For sensitive UIs, structural changes can greatly affect visual states. My tests confirmed "elements are rendered," but I missed the importance of CSS and layout states. How can you tell if HTML changes will affect the UI?
💀 It's crucial for tests to clearly define their scope.
Only in the develop
branch or if a teammate knows about the feature can you spot broken details. It's vital for the UI experience. To avoid breaking the UI and alerting me or a teammate about its importance, use Jest Snapshot testing.
As always, I prefer to use a scenario to better explain and understand the situation. Let's look at a scenario.
Scenario
You have been without an app for more than four months, but you pick a ticket to make a small change. The app shows a list of products.
My Product Test with Testing Library
I have an HTML markup that shows a list of products, the test use data-testid to find the title and ensure the element was rendered in the component.
<div class="products-list">
@for (p of products$ | async; track p) {
<div class="products-list__item">
<div class="products-list__item__title" data-testid="product-title">
<div>
<span>{{ p.title }}</span>
</div>
</div>
</div>
}
</div>
In my product.spec.ts
test file it find the products was render,
it('should render products', async () => {
await setup();
const productsTitle = screen.getAllByTestId('product-title');
const productTitles = productsTitle.map((title) =>
title.textContent.trim(),
);
expect(productTitles).toEqual(fakeProducts.map((product) => product.title));
});
The full source code
product.spec.ts
import { ProductsListComponent } from './products-list.component';
import { ProductModel } from '../../products/models/product.model';
import { ProductsService } from '../../products/services/products.service';
import { of } from 'rxjs';
import { render, screen } from '@testing-library/angular';
describe('ProductListComponent', () => {
const fakeProducts: ProductModel[] = [
//mock products.
];
const productsServiceMock = {
products$: of(fakeProducts),
};
it('should render products', async () => {
await setup();
const productsTitle = screen.getAllByTestId('product-title');
const productTitles = productsTitle.map((title) =>
title.textContent.trim(),
);
expect(productTitles).toEqual(fakeProducts.map((product) => product.title));
});
const setup = async () => {
await render(ProductsListComponent, {
providers: [{ provide: ProductsService, useValue: productsServiceMock }],
});
};
});
After run my test, looks like everything everything is working✅.
Something made noise, so I decided to remove unnecessary div
elements in the HTML markup. After making the changes, I ran my test, and everything looked perfectly green. ✅🥳
I open the application and it looks like everything works as expected and the tests are ✅ green... or not?
Did you found the diferencies ?
First, I have not touched this code for a few months, I didn't notice that something "ellipsis" in the text was removed. But why didn't my test notify me that I broke the app?
First, our test is focus in the app works and render the products, but don’t take care about the UI changes (its should be a element focus or other status), to avoid that kind of issues we will play with Jest Snapshot testing.
The Jest SnapShot
In a normal workflow, when we make changes in the UI and looks as expected, the process should be to open the browser, load the full app, and compare the UI. But what happens when it is a single component, like a row cell, a small row state, a toggle, or a card? sounds tricky ?
In these scenarios, this is where the Jest Snapshot shines! It helps us ensure your UI does not change unexpectedly. It compares the changes with a reference snapshot file stored. The test will fail when the two snapshots do not match, whether the change is unexpected or the reference snapshot needs to be updated with the new version.
Read more about SnapShot Testing
When we write a test using a snapshot, the result should be committed with our code change and in the next test run, Jest compares the rendered output with the previous snapshot, if nothing has changed, the test passes but if we change something like remove the div the test fails and shows the diff between.
Let's try to use it.
Moving to Jest SnapShot
First, I will restore the div
in my app and take another approach with my test to feel confident about whether I break the application UI. I need to change the setup function to return the fixture
, where I will take a reference for my snapshot.
const setup = async () => {
const { fixture } = await render(ProductsListComponent, {
providers: [{ provide: ProductsService, useValue: productsServiceMock }],
});
return {
fixture
};
};
Next, I will add a new test to ensure the UI changes don't break and it renders everything. I take the fixture
from the setup
function, then use the expect function with toMatchSnapshot
.
it('should display the correct list of product title', async () => {
const { fixture } = await setup();
expect(fixture).toMatchSnapshot();
});
Let's run test, it’s create the snapshot with the first version of the code.
After execution, an __snapshots__
the directory was created next product-list.component.spec.ts
Okay, if another teammate comes to work with the app and says, "Let me remove the div," let's try.
Yeah, our test complains that we broke the expected behavior because if my teammate created a snapshot, it is because this part of the structure is important for the project.
That's the key. If we have a test that protects the UI render, it is because it is important and makes us confident. At that moment, the teammate will find the differences and understand why it is failing.
Mmmm.. but it will break with every change?
We need to understand that Jest makes a diff between the generated output and the previous snapshot. So, if I add a new line or element, the UI might break. That is true. But my first mistake is writing tests that take on more responsibility than expected. So, it is very important to write a test that only takes care of the output.
In my code, I keep the first test responsible for finding the product name elements, but another test only takes care of the product list's structure.
But when I make intentional UI changes, your snapshot tests will fail because the rendered output no longer matches the stored snapshot. In these cases, you need to update the snapshots.
I can review the diff to ensure that the changes are indeed intentional and that they don't introduce any unintended visual issues. Because we have a snapshot, I pay attention to added, removed, or modified elements, attributes, and text content.
If everything looks as expected, then I can update the snapshots by running Jest with -u
.
npm test -u
Perfect! we have our snapshot updated and take care about changes in out layout! 🥳
They are not perfect 😭
If you have read up to this line, it is because you really want to know about snapshot testing. I made a big effort during the article to use the snapshot in atomic points in your application. My example focused on changing the structure and mentioned scenarios like cell, row, or atomic part in your app.
One of the most painful parts of snapshot testing is when it is used in a big component where many changes occur, and most developers skip and update the changes. We end up not caring about the snapshot because there are too many changes to read in a PR.
⚠️ So, I highly recommend please 🙏 read how to write effective snapshot and things to avoid with snapshots by kentcdodds.
Please avoid using snapshots with large components or with too many dependencies. Snapshots work best for simple components with clear, isolated functionality. That's why I mention examples like <row/>
or <app-icon/>
.
If you have the freedom in your I highly recommend cypress component testing, cypress visual testing or use modern testing like Vitest with Playwright Component Testing.
MODERN TESTING 🤔 VITEST.. 🧪 PLAYWRIGHT WITH ANGULAR?
If you're like me and want to learn modern testing practices in Angular, including working with Vitest, testing Observables, Forms, and Router, avoiding common mistakes with Mocks, Spies, and Fakes (like I did), and developing a solid testing strategy (which I need to improve), along with using Testing Library and Playwright Component Testing, all in one course,
I recommend checkout “Pragmatic Angular Testing” by Younes Jaaidi. He shares all the challenges of testing, lessons learned, and real-world experience with testing in Angular—all packed into one course.
Recap
After trying out snapshot testing, I now see how helpful it is to make sure our app's look and feel doesn't break by accident. We already use other tests to check if things work right, but snapshot tests help us see if things look right too. To explain snapshot testing to my son edgar, I said to him “Snapshot testing is like a picture of your UI”, remembering how a part of your app looked at one point. If it looks different later, the test will tell you.
But keep in mind the snapshot tests don't replace your other tests; they are another layer of safety for how things look and focus on testing small parts, which makes the tests easier to understand and less likely to break for the wrong reasons. When a snapshot test fails, I need to look at what changed in the "picture" to make sure it was supposed to change.
Overall, I think snapshot testing is really useful for parts of the app where how things are arranged and how they look to the user is important, and it stops unexpected visual problems before they reach our users, they are not perfect but help
I now think snapshot testing is a great way to help keep our app looking good and not breaking again 😊
Happy testing!
Subscribe to my newsletter
Read articles from Dany Paredes directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Dany Paredes
Dany Paredes
I'm passionate about front-end development, specializing in building UI libraries and working with technologies like Angular, NgRx, Accessibility, and micro-frontends. In my free time, I enjoy writing content for the Google Dev Library, This Is Angular Community, Kendo UI, and sharing insights here.