Snapshot testing or screenshot testing is a method in which the current output of your test is compared to a recorded “Snapshot”. This tutorial deep dives into Cypress snapshot testing, understanding how they are implemented and their advantages.
OVERVIEW
Testing has become an integral part of the fast-paced world of software development. In recent years, snapshot testing has gained popularity and has become a key player in this domain. With the constant changes being made to the front-end design of web applications, it is crucial to guarantee that these modifications maintain the smooth functioning of the current user interface.
When it comes to user interface modifications, there are instances where these changes may not directly impact the functionality of your web applications. Instead, they alter the visual aspects. These visual alterations are not easily detectable through unit testing or end-to-end testing, making snapshot testing necessary.
Snapshot testing serves this purpose by 'freezing' the user interface state for future comparisons. They identify unintended changes, keeping the application visually consistent across releases.
Generally, the snapshot testing feature integrates well with most automation testing frameworks, including Cypress, which is highly maintained and consistently gets regular updates with new features and plugins. This allows developers to write snapshot tests in their Cypress environment.
In this tutorial on Cypress snapshot testing, we will delve into the various categories of snapshot tests in Cypress, understanding how they are implemented and their advantages. We'll also discuss whether or not you should incorporate them into your Cypress tests.
To understand the importance of snapshotting in the Cypress automation framework, we need to understand what snapshotting is and what types we can use in Cypress.
Snapshot testing or screenshot testing is a method in which the current output of your test is compared to a recorded “Snapshot” (a stored version of an object or a UI at a specific time).
This helps keep track of the changes in your application over time and alerts you when unexpected changes occur.
When a snapshot test is run, the test will only pass if the current snapshot (object or UI) is identical to the reference snapshot saved earlier. In this way, if the test fails, there’s a discrepancy between the recorded snapshot and the current snapshot, meaning there are unexpected changes in the UI.
In this simple example above, Cypress will automatically detect discrepancies between the stored snapshot and the new web UI changes. This way, you can automatically detect the visual regression bugs without explicitly writing tests.
There are different types of snapshots we can use in our Cypress tests. Each has its differences and use cases; understanding these types will help us effectively capture the right moments for the best testing outcomes.
These three types of snapshots, each with their purpose and use, can be utilized effectively to enhance the resilience of your tests and their impact on your application.
Snapshot testing has rapidly gained popularity due to its unique ability to capture the state of a UI and verify it against a stored snapshot. This strength provides various benefits that solidify the importance of snapshot testing in your development cycle.
The key advantage of snapshot testing is to ensure consistency and integrity in your application’s UI across multiple iterations of development.
By comparing a UI's current visual state with a previously accepted state (a snapshot), snapshot testing reduces the risk of unexpected UI changes slipping through, thus maintaining the consistency and integrity of your application.
describe('Homepage', function() {
it('should render correctly', function() {
cy.visit('http://localhost:3000');
cy.get('#homepage').toMatchImageSnapshot();
});
});
With this small test suite you can easily detect any UI changes that can happen in the future without explicitly writing the tests for them.
Running Cypress snapshot tests is time-efficient because they are relatively faster to write and run than other tests. As a result, continuous manual updates are only necessary if there is an intentional UI change, thereby conserving valuable development time.
When developing new features or refactoring code, unintentionally altering the UI can be easy. With snapshot testing, unexpected UI changes can be caught early in development. This proactive detection of bugs allows for a quicker turnaround time for fixes.
However, snapshot testing is not meant to replace the other forms of testing in Cypress; it is just another tool in your toolkit for you to use whenever you think it is required.
The main purpose of visual snapshot testing is that it assures visual consistency and complements the other testing methods.
Let’s start writing some actual snapshot tests. This section is about using object-only snapshots in your Cypress snapshot tests. We will cover the visual snapshots later in the tutorial.
As we know, snapshots are like unit tests without the developer explicitly asserting the outcome.
For example, have a look at this code:
const greet = (name: string): string => 'Hello, ${name}!'
it("generates a greeting", () ={">"} {
expect(greet("Alex")).to.equal("Hello, Alex!")
})
This example is just a standard unit test with a simple assertion, we are expecting the result of the greet() function to be “Hello, Alex!” which is explicitly specified by the developer who wrote the test, this is because the developer already knew what the expected outcome should be.
Since this is a very simple unit test and the output is very simple, we don’t need to use snapshots for this test.
But now, let’s see a more complex real-world scenario. You want to write a unit test for an API response instead of returning a simple number.
The result is most likely a large object. Let’s have a look at the example below:
// Import axios
import axios from "axios"
axios
.get("https://example.com/posts")
.then((response) => {
// Handle the response
console.log(response.data)
})
.catch((error) => {
// Handle the error
console.log("An error occurred: ", error)
})
Response example:
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit"
},
...
]
In this case, what most developers would do before snapshotting was to know already what the response of the API request would be, and they’d had to copy/paste the JSON response or any other data type to their test suite.
When dealing with tests, managing the bloated and unwieldy nature of the responses, particularly when they are of significant size, is challenging. It can also become tedious since each API call requires sending a request, copying the response, and pasting it for examination.
As you can see, this is a tedious task, and it will easily make the code bloated and difficult to maintain, especially if we have a large response object.
This is where the snapshot testing comes into place. Here is an example of how we can do the same thing with a snapshot.
it('returns a list of posts', () => {
getData().then(item => {
cy.wrap(item).toMatchSnapshot()
})
})
If the values are identical (using deep equality), the test passes. If they differ, the test throws an error.
The first time this test is run by Jest (or Ava or Mocha with the snap-shot-it plugin), it saves the received value into a snapshot file. This saved value can be reviewed and then committed with the spec files. On subsequent test runs, Jest will load the value from the snapshot and compare it with the received value. The test passes if the two values match (using deep equality). If they differ, the test throws an error.
Snapshot testing is not only limited to objects and static data. It can also be used to snapshot specific web elements during end-to-end tests, allowing you to track and compare changes in these elements over multiple test runs.
It also adds depth to the debugging process by providing a detailed picture of what happened at each step of your test. In the next section, we'll explore the importance of this feature and provide some concrete examples.
Element snapshotting in Cypress captures the state of a DOM (Document Object Model) element when the snapshot is taken.
This includes the element’s styles, properties, and position in the DOM tree. It's like a photo taken mid-action, capturing the "living" state of the web application as interactions occur.
For this purpose, we are going to use a plugin called cypress-plugin-snapshots.
Consider the following code:
describe('Spec test', () => {
beforeEach(() => {
cy.visit('https://www.lambdatest.com/selenium-playground/input-form-demo')
})
it('should create the snapshot successfully', () => {
cy.get('h1').toMatchSnapshot()
})
})
In this code, we're capturing a snapshot of the DOM element h1. We can now inspect the snapshot and see the state of the element at the time this line was executed.
The snapshot is now saved into a JavaScript folder in the root folder of your Cypress test cypress\e2e\__snapshots__\<file-name>.cy.ts.snap
Let’s have a look and see how the snapshot is stored:
exports['H1 snapshot > Should snapshot the H1 element #0'] = '
<h1
class="text-size-48 font-bold text-black text-center leading-none text-shadow md:w-full leading-height-70 mx-auto smtablet:text-size-30 smtablet:leading-height-42"
>
Form Demo
</h1>
';
The format of the saved snapshot is in the following pattern:
exports['<describe-block> > <test-block> #<number-of-snapshot>'] = '
<snapshotted-element>
';
Sample Scenario:
Let's consider a scenario where we have a button with a specific class called btn.
<button class="btn">Press Me</button>
Let’s write the Cypress test for it so that we can store the snapshot and compare it to it later in future tests.
it("Button matches snapshot", () => {
cy.get(".btn ").toMatchSnapshot()
})
If you run it for the first time, you’ll see a message in the Cypress console which says “SNAPSHOT UPDATED” which means the snapshot for this element is successfully stored inside the cypress\e2e\__snapshots__ folder.
And here’s how the snapshot is stored.
exports['Spec test > Button matches snapshot #0'] = '
<button class="btn">Press Me</button>
';
Running it for the second time without changing the element:
The test passed successfully since the element stayed the same and matched the stored snapshot. You’ll see a message in the Cypress console “SNAPSHOTS MATCH”.
Let’s try changing the element and see if we get the desired error. We will change the text in the button, this is how it looks now:
<button className="btn">Press</button>
Let’s rerun the test.
As you can see, we are getting an error in the Cypress console, complaining that there is a snapshot difference.
This is a great way to keep our applications consistent and be warned whenever an element in the DOM changes unexpectedly.
In this section, we will learn how to install the necessary plugins for Cypress to run snapshot tests. We will also try out all snapshots, including visual snapshot testing.
Once we've completed that step, we'll leverage the robust cloud testing capability for Cypress. This will allow us to automate and execute our tests in the cloud, streamlining the delivery process and enabling compatibility across various browsers.
Let’s get started.
First, let’s initialize a Node.js project by running the following command:
npm init -y
This will create a package.json file with the following configurations:
{
"name": "cypress-snapshot-testing",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
If you want to use TypeScript with Cypress, then run the following commands to use TypeScript with Cypress:
Install TypeScript:
yarn add -D typescript
Initialize TypeScript:
npx tsc --init
This will create a tsconfig.json file in your root directory.
After that, we need to install Cypress and run it locally. Install the latest version of Cypress using the following command:
yarn add -D cypress@latest
We also need to update the scripts object inside the package.json file to run Cypress with a more concise command:
...
"scripts": {
"test": "cypress open"
},
...
Open up the Cypress test runner with the following command:
yarn test
Or
npm run test
After the Cypress window opens up, select E2E testing:
Choose a browser you prefer for your tests:
Create a new spec file:
Great, now our spec file has been created by Cypress and is ready to be modified and run our tests in it.
For this Cypress tutorial, all our tests will be run on the LambdaTest Playground for testing purposes.
To make things easier, we can update the Cypress config file and add our website as the baseUrl key.
// ./cypress.config.ts
import { defineConfig } from "cypress"
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: "https://www.lambdatest.com/selenium-playground/simple-form-demo",
},
})
To utilize the snapshotting features with Cypress, we need to install a plugin named cypress-plugin-snapshots.
cypress-plugin-snapshots: It helps integrate snapshot testing into Cypress, allowing developers to easily capture and compare UI states, objects, and DOM elements, ensuring consistency across your application.
Install the cypress-plugin-snapshots plugin.
yarn add -D cypress-plugin-snapshots
Import it to the commands.js/ts file.
import 'cypress-plugin-snapshots/commands';
Import it as a plugin inside the <cypress.config.js/ts> file:
import { defineConfig } from "cypress"
import { initPlugin } from "cypress-plugin-snapshots/plugin" ← import the plugin
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
initPlugin(on, config)// ← initialize the plugin
},
baseUrl: "https://www.lambdatest.com/selenium-playground/simple-form-demo",
},
})
If you want to benefit from the TypeScript autocomplete features, update your commands.js/ts file and add this:
declare global {
namespace Cypress {
interface Chainable {
toMatchImageSnapshot(args?: {
imageConfig?: {
createDiffImage?: boolean
threshold?: number
thresholdType?: "pixel" | "percent"
}
name?: string
separator?: string
}): Chainable<Element>
toMatchSnapshot(args?: {
ignoreExtraFields: boolean
ignoreExtraArrayItems: boolean
normalizeJson: boolean
replace: {
[key: string]: string | number | boolean | null | undefined
}
}): Chainable<Element>
}
}
}
Here is a quick explanation of each property that you can provide for the toMatchImageSnapshot() method:
createDiffImage: When set to true, this option generates a "diff image" highlighting the differences between reference and new snapshots. It's a visual tool for identifying changes, but it can be set to false to skip this process and improve performance.
threshold: This parameter sets the tolerance level for differences between the reference and new snapshots. If the differences exceed this amount (in pixels or percentage), the snapshot is considered invalid.
thresholdType: Specifies whether the threshold is measured in "pixel" or "percent." It defines the unit for the threshold parameter, helping to fine-tune the sensitivity of the comparison.
name: Allows the resulting image file to be named with a custom name rather than concatenating test titles. It aids in organizing and identifying specific snapshot files.
separator: Enables using a custom separator in naming the resulting image file, rather than using the default separator, "#”. It provides additional flexibility in structuring and labeling the snapshots.
toMatchSnapshot() properties:
ignoreExtraFields: In subsequent tests, if the new version of the object has extra fields (that weren't present in the first snapshot) and ignoreExtraFields is set to true, the test will not fail because of these additional fields.
ignoreExtraArrayItems: If set to true, for array types of snapshots, the test will not fail if there are additional items at the end of the array in the current test data compared to the snapshot.
normalizeJson: Alphabetically sorts keys in JSON.
replace: Replaces a key inside of the snapshot with the provided value.
Now that we’ve set up our Cypress environment with the necessary snapshotting plugins and types, it’s time to write some real-life snapshot tests.
Since the website we’re practicing on is hosted and can not change the UI or the front-end code, we’ll try to mimic real-life examples to do snapshot testing by changing the application's state, not by changing the code.
describe("Snapshot testing", () => {
beforeEach(() => {
cy.visit("https://www.lambdatest.com/selenium-playground/simple-form-demo")
})
it("should match the snapshot", () => {
cy.get("input#user-message").type(
"Lambdatest is a cloud-based platform enhancing testing with its scalable, reliable, and secure global infrastructure. It offers cross-browser testing capabilities and continuous integration tools."
)
cy.get("button#showInput").click()
cy.get("div.mt-30.rounded")
.first()
.toMatchImageSnapshot()
})
})
In this example for visual testing using Cypress, we’re simply visiting the playground website to run our tests on, then we select the input and submit the form to see the message appearing next to the text input.
After that, we get the user message element and take a snapshot using the toMatchImageSnapshot() function with the cypress-plugin-snapshots plugin.
Let’s run the test:
The first time the test is run, the cypress-plugin-snapshots will save the image snapshot in the folder <cypress/e2e/__image_snapshot__>.
We can change the name of the snapshot by adding the name property, for example:
cy.get("div.mt-30.rounded")
.first()
.toMatchImageSnapshot({
name: "User message",
})
The snapshot will then be saved in the <cypress/e2e/__image_snapshot__/User message #0.png> file.
Let’s change the UI to get a mismatched snapshot error from the Cypress snapshots plugin.
We will change the text in the input just a bit to see if we get the snapshot error.
Here’s our next test:
describe("Snapshot testing", () => {
beforeEach(() => {
cy.visit("https://www.lambdatest.com/selenium-playground/simple-form-demo")
})
it("should match the snapshot", () => {
cy.get("input#user-message").type(
"Another website is a cloud-based platform enhancing testing with its scalable, reliable, and secure global infrastructure. It offers cross-browser testing capabilities and continuous integration tools."
)
cy.get("button#showInput").click()
cy.get("div.mt-30.rounded")
.first()
.toMatchImageSnapshot({
name: "User message",
})
})
})
The only thing that is changed is the text <LambdaTest> is changed to <Another website.>
Running the test:
Weird, isn’t it?
It should have thrown an error instead of passing the test because we changed the text; therefore, the UI should also change.
This test is considered a passed test because the cypress-plugin-snapshots plugin has a property called threshold, which is set to 0.01 by default.
This means for Cypress to throw an error, there should be at least a one percent change on the UI, and in our case, the change is very small and lower than one percent; therefore, it’s not throwing an error.
Let’s tweak the threshold property to 0. This way, the test will only pass if the images are 100% identical.
cy.get("div.mt-30.rounded")
.first()
.toMatchImageSnapshot({
name: "User message",
imageConfig: {
threshold: 0,
},
})
Test Execution:
As you can see, the test is failing as expected. Look at the <cypress/e2e/__image_snapshot__> directory and the generated images.
Our snapshot image directory looks like this now:
Let’s look at the <.diff.png> image file and see how it looks.
As seen above, the cypress-plugin-snapshots does a pretty good job of spotting the differences between the two snapshots.
This is a reliable way to write tests to make the UI match your expectations. Without visual snapshot testing, it would be impossible to write tests for the aesthetics of your website.
You can also integrate visual snapshot testing into your CI/CD pipelines to fortify your deployment process. This will automate detecting unintended UI changes, ensuring functionality and aesthetics are on point before any release.
Sometimes, we expect a very large object to come back from the API we use for our tests. The problem is that sometimes, the response we get from the API calls we make could be huge, and it would be very unreliable to write the object down in the test suite.
The best way to handle this is to save the object as a snapshot expected to be returned from the API call and compare it to the object returned in the subsequent test runs.
Example:
Now, let’s write a test to save a JavaScript object as a snapshot and compare it in the subsequent test runs.
To send API requests in our Cypress tests, we need to install the Axios library, or you can use the fetch API in the browser.
Install Axios:
yarn add axios
Since this is a different test, we will create a spec file for this test called <cypress\e2e\api.cy.ts>.
Sending the API request with Axios.
import axios from "axios"
describe("API response snapshot tests", () => {
beforeEach(() => {
cy.visit("https://www.lambdatest.com/selenium-playground/simple-form-demo")
})
it("should match the snapshot", async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
)
})
})
Saving the snapshot by using the .toMatchSnapshot() function that comes with the cypress-plugin-snapshots npm package.
import axios from "axios"
describe("API response snapshot tests", () => {
beforeEach(() => {
cy.visit("https://www.lambdatest.com/selenium-playground/simple-form-demo")
})
it("should match the snapshot", async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
)
cy.wrap(data).toMatchSnapshot()
})
})
Test Execution:
Our test has run successfully with the snapshot saved in the <cypress\e2e\__snapshots__> folder.
It will save it in a snapshot file with the same name as the spec file with the suffix “.snap”, in this case, “api.cy.ts.snap”.
This is how the data is stored in the snapshot file <cypress\e2e\__snapshots__\ api.cy.ts.snap>.
exports['API response snapshot tests > should match the snapshot #0'] =
[
{
"completed": false,
"id": 1,
"title": "delectus aut autem",
"userId": 1
},
{
"completed": false,
"id": 2,
"title": "quis ut nam facilis et officia qui",
"userId": 1
},
…
];
This way the snapshot is stored perfectly and we can detect changes in the subsequent tests if there were any expected changes.
Let’s try to change the API response by changing the API endpoint to something that returns a different response.
import axios from "axios"
describe("API response snapshot tests", () => {
beforeEach(() => {
cy.visit("https://www.lambdatest.com/selenium-playground/simple-form-demo")
})
it("should match the snapshot", async () => {
// First API Call
// const { data } = await axios.get(
// "https://jsonplaceholder.typicode.com/todos"
// )
// Second API Call
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
)
cy.wrap(data).toMatchSnapshot()
})
})
Test Execution:
As expected, we’re getting an error message from Cypress because the snapshots didn’t match. Capturing objects in Cypress helps us easily detect changes, especially in API calls, without writing the expected values ourselves and making the code bloated.
Now, let’s write some tests for the same page but for element snapshotting. This way, we can easily save a serialized version of the elements in the DOM and be warned if there are any unexpected changes in the elements when running any subsequent tests.
Serializing an HTML element transforms an HTML element to string format to be compared to the same serialized element in subsequent tests.
Writing our element snapshot test:
describe("Element snapshot tests", () => {
beforeEach(() => {
cy.visit("https://www.lambdatest.com/selenium-playground/simple-form-demo")
})
it("Should match the snapshot of the element", () => {
cy.get("h1").toMatchSnapshot()
})
})
This way it will get the only h1 element in the DOM and save the serialized version of it as a snapshot.
Let’s run it.
Now, the snapshot is saved inside of the <cypress\e2e\__snapshots__> folder.
Here’s how the snapshot is stored:
exports['Element snapshot tests > Should match the snapshot of the element #0'] = '
<h1
class="text-size-48 font-bold text-black text-center leading-none text-shadow md:w-full leading-height-70 mx-auto smtablet:text-size-30 smtablet:leading-height-42"
>
Simple Form Demo
</h1>
';
Now, anytime the h1 element has changed, Cypress will throw an error, and the test will fail.
Let’s replicate this scenario by getting another element from the DOM and comparing it to the stored element.
describe("Element snapshot tests", () => {
beforeEach(() => {
cy.visit("https://www.lambdatest.com/selenium-playground/simple-form-demo")
})
it("Should match the snapshot of the element", () => {
// First snapshot
// cy.get("h1").toMatchSnapshot()
// Second snapshot
cy.get("h2")
.first()
.toMatchSnapshot()
})
})
Running the test:
As expected, Cypress shows the difference between the snapshots beautifully.
Note : Automate Cypress visual tests with Smart UI. Try LambdaTest Now!
Before discussing Cypress snapshot testing on the cloud, it is important to understand visual regression testing.
Visual regression testing is an approach used in software testing to detect unintended alterations in a web application's user interface (UI). It specifically aims to maintain the consistency and integrity of visual components, including layouts, styles, and graphics, throughout the development or maintenance phases. The main objective is to identify visual inconsistencies between different versions after modifying the underlying code.
By incorporating visual regression testing into the testing process, development teams can enhance the quality assurance process, reduce the likelihood of visual defects, and deliver a more consistent and visually appealing user experience.
Unlike snapshot testing, visual regression testing is tailored to monitor and ensure the visual integrity of an application, making it especially relevant for UI-centric projects. By combining test automation and image processing, visual regression testing checks that our application looks as it should.
A key benefit of performing Cypress visual regression testing in the cloud is that it allows for comprehensive visual testing across different browsers and operating systems without needing to change the core logic of the test code.
By using a cloud-based AI-powered test orchestration and execution platform like LambdaTest, you can perform automated visual testing on the cloud Cypress grid. LambdaTest also enables Cypress parallel testing, speeding up the entire testing process. This approach enhances visual test coverage, ensuring a higher quality web product from a visual perspective.
By leveraging cloud capabilities and visual testing tools like Cypress on LambdaTest, you can perform automated testing to identify visual bugs and ensure the delivery of pixel-perfect websites and web apps.
However, you can visit the LambdaTest YouTube channel for more videos on Cypress UI testing, automation testing, Selenium testing, etc., so there’s never a shortage of learning opportunities.
In conclusion, Cypress snapshot testing is a robust method for validating the visual aspects of your applications, fortifying the resilience of your comprehensive testing strategy. By capturing UI states, this method serves as an essential strategy for detecting unexpected visual changes, elevating the quality and reliability of your applications.
However, as with any testing tool, it's important to remember that Cypress snapshot testing isn't for all testing needs. It excels in providing visual validation and supplementing other types of testing, like unit and end-to-end tests. Hence, snapshot tests should be a part of a larger, comprehensive testing strategy that covers different layers of your application.
Now that you've got a solid grasp of Cypress snapshot testing and its potential impact, it's time to start experimenting. So set up Cypress, write your first snapshot test, and enhance your testing suite one snapshot at a time!
Dawson is a full-stack developer, freelancer, content creator, and technical writer. He has more than three years of experience in software engineering, and he is passionate about building projects that can help people.
Get 100 minutes of automation test minutes FREE!!