Let me paint you a picture. Youâre working on an app you donât know too well, and you want to make sure you havenât broken anything with whatever wide-reaching change youâve made. The QA department is on vacation, and they hate doing those full regression tests anyways. Besides, you need to get those totally safe changes out to your users instantly!
From one overpaid developer to another, Iâve broken production so many times because I didnât think of some weird edge case, or didnât bother to run through all the manual tests before I hit the big red deploy button. Itâs too much of a hassle, I probably wouldnât have caught the error anyways, and sometimes Iâm just lazy.
I had heard of end-to-end tests previously, but they were always talked about as those flaky, hard-to-get-running, impossible-to-keep-up-to-date monstrosities we just ignored. Selenium was free and terrible, and the other options on the market were thousands of dollars (and probably not all that much better). So automating those boring regression tests wasnât really an option either.
So imagine my skepticism when I started hearing about this new tool that supposedly made end-to-end testing a breeze. It was called Cypress, and I wrote it off as âyet another Selenium with a great marketing teamâ for the longest time. Surely, it wasnât even worth exploring.
One day, though, I was tasked with doing a proof of concept on how we should write end-to-end and integration tests for an app at work. The backlog was closing in on empty, and it was definitely time to improve the test coverage of our app. So perhaps it was finally time â letâs give Cypress a shot. This article will convince you to do the same.
In this article, weâll go through Cypress, Cypress Testing Library, and Axe â three tools that together will give you real confidence that your application works as expected.
So before we get into the nitty-gritty details, letâs look at what these three tools are, and how they help you towards your goal of creating better apps.
Cypress is a tool for creating and running end-to-end tests. It spins up a browser, visits your app, and runs through a set of predefined steps like a regular user would. Finally, it verifies that the result is what youâd expect.
These kinds of tests are slow compared to unit and integration tests, but they do an amazing job of making sure your app works as expected for the end user. You shouldnât write too many of them but instead, aim for covering the main paths of your app.
Cypress Testing Library is a library that plugs into Cypress and makes it easy to write tests that encourage accessible code. It does away with one of Cypressâ pitfalls â the way you select elements â and provides you with an API you probably know from unit testing with React Testing Library, Vue Testing Library, or other libraries in the same family.
Finally, Axe is a tool for verifying that your app is both accessible and WCAG compliant. Itâs available as a browser plugin, but it only verifies how your app looks right now â not after 13 interactions and a navigation event. Luckily, the combination of Cypress and Axe makes that last part a breeze, too â Iâll show you how later in this article.
So weâve found a toolchain that looks promising â but how do we get set up? First, letâs install the tools from npm:
npm install cypress @testing-library/cypress cypress-axe --save-dev
This will create an empty cypress.json file, as well as a cypress folder with a few configuration files and examples in it.
The default cypress.json is empty, because Cypress ships with really sensible defaults. Thereâs a lot of ways to customize it though! We wonât do a lot now, but you might want to specify the base path of your application, so you donât have to start every test by navigating to it. Thatâs done by setting the baseUrl option:
{
"baseUrl": "http://localhost:3000",
}
Cypress has this concept of custom commands that you can call while testing, and both Cypress Testing Library and cypress-axe provide you with a few extra âcommandsâ. So to get them set up, navigate to the newly created file cypress/support/commands.js and change its content to the following:
import '@testing-library/cypress/add-commands'; import 'cypress-axe';
If youâre using ESLint, you might want to create an .eslintrc file in the cypress folder with the following content:
module.exports = {
root: true,
plugins: ['eslint-plugin-cypress'],
extends: ['plugin:cypress/recommended'],
env: { 'cypress/globals': true },
};
If youâre using TypeScript, you want to add a custom tsconfig.json in the cypress folder as well:
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}
You also need to create a type definition file to include the types for cypress-axe. Weâll call it cypress/support/index.d.ts, and fill it with this:
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
injectAxe(): Chainable<EventEmitter>;
checkA11y(): Chainable<EventEmitter>;
}
}
Finally, letâs add two new scripts to the package.json file, so we can run our tests, too:
{
"scripts": {
"test:e2e": "cypress open",
"test:e2e:ci": "cypress run"
}
}
The test:e2e script opens up Cypressâ built-in UI, which lets you trigger tests and inspect them step for step. The test:e2e:ci runs the tests in a headless browser â perfect for running as a part of your continuous integration pipeline.
So the setup is done and writing the tests remains. But what makes for a good end-to-end test case?
As I mentioned initially, you shouldnât really have too many of these end-to-end tests. They are slow, resource intensive, and require you to keep stuff up to date. Therefore, you should focus on the main paths of your application, and leave the rest to your integration and unit tests.
By following this approach, you can add tests for all of the main paths of your application, while still keeping your test suite fast enough for running often.
Enough setup and theory â letâs get to testing! Weâll be interacting with an imagined application to keep things simple.
First, we create a new file in our cypress/integration folder, which weâll call todo-tests.ts. Weâll start with adding a test for adding a todo to our list, which I guess is the main use of todo apps đ
It will look like this:
describe('todo', () => {
beforeEach(() => {
cy.injectAxe();
})
test('adds a todo', () => {
cy.checkA11y();
cy.findByText("Learn Cypress").should('not.exist')
cy.findByLabelText(/What do you want to do/i)
.type('Learn Cypress{enter}');
cy.findByText("Learn Cypress").should('exist')
cy.checkA11y();
})
})
Thereâs a lot going on here, so letâs go through it step by step.
cy is the variable we interact with to run our tests. Itâs the one weâve added all the custom commands to previously!
First, we make sure to call the injectAxe function on it before we start every test so that the axe plugin is loaded and ready to rumble.
We start our first test by calling cy.checkA11y(). This runs a thorough accessibility audit on your app in its current state. Next, we make sure the todo âLearn Cypressâ isnât added to the list before we begin.
Now, itâs time to add our todo. We find the input field by looking up its label with cy.findByLabelText. This is a great extra check to make sure our input field is accessible!
We type into the input field by calling the .type method with the text we want. We can trigger the enter button by writing â{enter}â. This also makes sure weâve placed our input field inside of a <form/> tag.
After adding the todo, we want to make sure the âLearn Cypressâ-todo is added to our list. We use the findByText function to look it up, and assert that it should exist.
As a final touch, we check that the app is still accessible with a todo item added.
There are a few more tests I might want to add. I want to make sure I can set a todo as done and that I can filter out the ones Iâve already done. Perhaps I want to test that I get an error message if I try to add an empty todo?
For brevityâs sake, I wonât go through any more tests in this article, but they all follow the same pattern. Make sure to check your app is always in an accessible state, and use accessible selectors that nudge you towards writing accessible apps from the get-go.
We could very easily have verified that our app works by manually going through these steps in a browser. These automated end-to-end tests arenât really revolutionizing anything â but they sure are incredibly handy!
If youâre lucky enough to have a QA engineer on your team, theyâll love you for writing these sorts of tests too. You wonât take their job away â but youâll help them automate the part of their job that is tedious and mundane!
As a matter of fact, you can bring test-driven development to a new level. Instead of focusing on small features, you can create a specification of what you want your app to do, and you can let your tools verify your progress as you build out your app!
Manual verification is a thing of the past. Itâs a brittle technique that is perfect for forgetting edge cases and hard-to-reach states. Instead, get with the program and join the revolution that is Cypress. Youâll be able to know your app works as expected for your users without actually checking it yourself. Youâll know the main flows of your application is working, and youâll know it will work for people using assistive technologies as well. And finally, youâll know youâll pass any accessibility review since accessibility is built into how you write your tests.
I hope this article got you as fired up as I am about testing. Not because testing is cool, but because itâs extremely boring. So skip the manual testing and automate it all away.
My favorite resource on Cypress is Kent C Doddsâ course on TestingJavaScript.com. Itâs a great investment in your testing skills, and I could not recommend it enough. The official Cypress docs are also a great read, and includes real-world examples, getting started guides and lots of neat tutorials.
Best of luck!
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Learn how to use browser AI APIs for summarization, translation, and language detection with practical examples and implementation tips.

Build a live task board with Topcoat and Rust, using reactive signals, server-rendered search, and WebSocket updates.

Learn how to build and install three Claude Code mods to monitor usage, protect secrets, and track changes.

I tested Jev against popular LLMs to compare speed, cost, and accuracy on real bank support messages.
Hey there, want to help make our blog better?
Join LogRocketâs Content Advisory Board. Youâll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now