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 in place to validate my code, and I was confident that the data was rendering correctly. What I didn’t consider were the child components and UI states—like :hover
or :focus
. These states depend heavily on the structure of the DOM and CSS rules.
If you're working on a sensitive UI—where visual states and effects are just as important as content—then structural changes can have a bigger impact than expected. I trusted my test, which confirmed that "the elements are rendered," but I didn’t realize how important the CSS rules and layout states were. So how do you know if a change in the HTML will impact the UI?
💀 This is why it’s so important that your tests clearly define their scope.
At that moment, only in the develop
branch or if another teammate knows about that feature is the only way to know the small details were broken. How the team knows that is important and maybe critical for the UI experience. So the only way to avoid breaking the UI and to inform me or another teammate in the future that this is important is by protecting it using 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!
Top comments (0)