The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Editor’s note: This post was updated on 19 January 2021 to reflect the changes and improvements introduced with Cypress 6.1.0.
Before I start, I want to emphasize that this post is not about one particular project or any automation testers that I have worked with. I have seen this behavior in three recent projects, and nearly every automation tester that I have worked with has busted a gut to make this faulty machine work.
I am fairly sure that a memo has gone out to every contract that I have worked on recently stipulating that a million automation tests are required to guarantee success. We must not stop to question the worth of these tests. We must protect them like our children.
These tests must be written in Selenium, despite nearly everyone having a pretty grim experience due to the inherent known issues that I will state later. According to their docs, Selenium provides a range of tools and libraries to support the automation of web browsers and provides extensions that emulate user interaction with browsers, as well as a distribution server for scaling browser allocation. It also has the infrastructure for implementations of the W3C WebDriver specification that lets you write interchangeable code for all major web browsers.
Selenium tests are insanely challenging to write, but we won’t let that hold us back. Instead, we will get our testers who have maybe come into programming late or are new to development. We’ll get these less experienced developers to write these difficult tests.
Selenium tests might be difficult to write, but they are straightforward to copy and paste. This, of course, lead to all sorts of problems.
We often hear, “If it moves, write a Selenium test for it”. Automation tests must be written for the API, the frontend, the backend, the middle-end, the happy path, the sad path, the upside-down path, etc.
We won’t have any time for manual testing, and how could we? We have all these flakey Selenium tests to write and maintain. We are already late for this sprint, and every story must have an automation test.
After a year or so and an insanely long build, we will decide that this was a bit silly and delete them all. Or worse — start again.
I think I would be closer to understanding the true nature of our existence if I could answer the above question. All jokes aside, why is the use of Selenium so widespread? It does stagger me, but here are a few suggestions:
To be fair, the sudden surge of writing a million acceptance tests is not Selenium’s fault. For my money, the correct number of automation tests is one happy path test, no sad paths or upside-down paths. This one test is a smoke test to ensure that our system is open for business.
Unit tests and integration tests are cheaper to run, implement, and maintain and should be the bulk of our tests. Has everyone forgotten about the test pyramid?
The problems with Selenium can be expressed in one word: timing.
Before we can even start writing code to assert that our test is correct, we need to ensure that whatever elements we need to interact with are visible and are in a state to accept simulated input. Remote API calls will need to have resolved, animations and spinners need to have concluded. The dynamic content that now makes up the majority of our apps will need to have finished rendering from the currently retrieved data of the API calls.
So what do we do while this macabre pantomime of asynchronicity is occurring? How do we stop our tests from just finishing or bottoming out because a particular text input is disabled until an API call has finished or a beautiful SVG spinner overlay has put a veil of darkness over our virtual world?
In layman’s terms, we wait for the HTML elements to be in a ready state. In Selenium speak, we write many custom waitForXXXXX code helpers, e.g.
waitForTheFullMoonAndTheWereWolvesHaveFinishedEating or more realistically…
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//input[@id='text3']")));
One of the worst crimes to commit is to use Thread.sleep. This is a heinous crime where a random number is plucked from thin air and used as a wild guess for when we think the UI is in a ready state. Please, never do this.
Below are my all-time favorite Selenium exceptions that I have found while wading through a CI build report:
NoSuchElementException – move along, you’ll not find your input hereElementNotVisibleException – this cheeky scamp means you are tantalizingly close but not close enough, it is in the DOM, but you can’t do a single thing with itStaleElementReferenceException – the element has finished work for the day and gone to the pub. Please try again tomorrowTimeoutException – you could wait until the end of time and whatever you are trying to do is just not going to happen. You just rolled a sevenOne of the most soul-destroying moments that I have experienced is having a build fail due to a failing automation test only for it to magically pass by just rerunning the build again. This phenomenon or zombie automation test is often referred to as a flake.
The main problem with the flake is that it is non-deterministic, which means that a test can exhibit different behavior when executed with the same inputs at different times. You can watch the confidence in your regression test suite go up in smoke as the number of non-deterministic tests rises.
A flakey test is more than likely down to timing, latency and the macabre opera of asynchronicity that we are trying to tame with our Thread.sleep and waitForAHero helpers that we need to keep writing to try and keep sane.
Just think how much easier this would be if we could somehow make all this asynchronous programming go away and if our world started to behave linearly or synchronously. What a natural world to test we would have.
Cypress.io sets out to do just that.
Cypress is a JavaScript-based framework for end-to-end testing. It’s built on top of Mocha and runs in the browser, enabling asynchronous testing. According to the Cypress docs, Cypress can help you write integration tests and unit tests in addition to end-to-end tests.
Cypress includes the following features:
One of the main differences between Cypress.io and Selenium is that Selenium executes in a process outside of the browser or device we are testing. Cypress executes in the browser and in the same run loop as the device under test.
Cypress executes the vast majority of its commands inside the browser, so there is no network lag. Commands run and drive your application as fast as it is capable of rendering. To deal with modern JavaScript frameworks with complex UI’s, you use assertions to tell Cypress what the desired state of your application is.
Cypress will automatically wait for your application to reach this state before moving on. You are completely insulated from fussing with manual waits or retries. Cypress automatically waits for elements to exist and will never yield you stale elements that have been detached from the DOM.
This is the main take away. Cypress has eliminated the main problem with Selenium by executing in the same run loop as the device. Cypress takes care of waiting for DOM elements to appear.
I repeat: Cypress takes care of all this waiting business. No Thread.sleep, no waitForTheMoon helper. Don’t you see what this means?
To really grasp how good this is, you have to have experienced the pain.
Below are a few examples of Cypress tests.
One thing synonymous by their absence is any timing or obscene waitFor helpers:
context("Login", () => {
beforeEach(() => {
cy.visit("localhost:8080/login");
});
it("can find and type in email", () => {
cy.get("#email")
.type("[email protected]")
.should("have.value", "[email protected]");
});
it("can find and type in password", () => {
cy.get("#password")
.type("fakepassword")
.should("have.value", "fakepassword");
});
it("will fail when type invalid user credentials", () => {
cy.get("#email").type("[email protected]");
cy.get("#password").type("fakepassword");
cy.get("input[type=submit]").click();
cy.get("#login-message").should("have.text", "Login failed");
});
});
I like these tests. They clearly state their purpose and are not obfuscated by code that makes up for the limitations of the platform.
Below are some tests I wrote to run the axe accessibility tool through Cypress:
import { AxeConfig } from "../support/axeConfig";
describe("Axe violations", () => {
beforeEach(() => {
cy.visit("/");
cy.injectAxe();
});
it("home page should have no axe violations", () => {
cy.configureAxe(AxeConfig);
cy.checkA11yAndReportViolations();
});
});
And here is a similar test using webdriver:
// in e2e/home.test.js
import assert from 'assert';
import { By, until } from 'selenium-webdriver';
import {
getDriver,
analyzeAccessibility,
} from './helpers';
describe('Home page', () => {
let driver;
before(() => {
driver = getDriver();
});
it('has no accessibility issues', async () => {
await driver.get(`http://localhost:3000`);
// The dreaded wait until. Abandon hope
await driver.wait(until.elementLocated(By.css('h1')));
const results = await analyzeAccessibility();
assert.equal(results.violations.length, 0);
});
});
The main striking difference and the worrying thing to me is the latency. There are two await calls and the dreaded wait(until.elementLocated). This is a simple test, but the more interactions you have, the more waitFor helpers you will need, and the flakiness starts spreading.
Here’s a tutorial for writing end-to-end tests in Cypress if you’re interested in learning more.
Cypress is clearly aimed at the frontend developer. Installing Cypress is a breeze and performed via your favorite package manager choice: npm or yarn.
npm install cypress --save-dev
It really could not be any easier. Compare that with downloading the Chrome WebDriver and friends in the world of Selenium.
There is no multi-language support like Selenium. You can have any programming language you like as long as it is JavaScript or TypeScript.
Of course, there are drawbacks, and some of them are notable so I would be remiss not to list these.
It’s also important to note that Cypress does not support native mobile apps. However, you can use Cypress to test some functionality of mobile web browsers and test mobile applications that are developed in a browser using frameworks like Ionic.
As much as I would like to say yes, I have my doubts. There is an army of automation testers who have not known any other world than selenium, and it may be difficult to move away from soon.
While Cypress introduces a compelling new testing framework, it’s important to take testing one step further. LogRocket monitors the entire client-side experience of your application and automatically surfaces any issues (especially those which tests might have missed). To gain valuable insight into production environments with frontend monitoring, try LogRocket. 
LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on performance issues to quickly understand the root cause.
LogRocket instruments your app to record requests/responses with headers + bodies along with contextual information about the user to get a full picture of an issue. It also records the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Make performance a priority – Start monitoring for free.
As I stated at the start of this article, my experience with automation testing is not a good one. A lot of money, time, and pain are spent keeping thousands of hard-to-maintain tests afloat for a less-than-gratifying payout. Automation testing has only ever guaranteed a long CI build in my experience.
We, as developers, need to be better at automation testing. We need to write fewer tests that do more and are useful. We’ve left some of the most difficult code to write to some of the least experienced developers. We’ve made manual testing seem outdated when, for my money, this is still where the real bugs are found.
We need to be sensible about what automation testing can achieve.
Cypress is great because it makes things synchronous. This eliminates a whole world of pain, and for this, I am firmly on board. This, however, is not the green light to write thousands of Cypress tests. The bulk of our tests are unit tests with a layer of integration tests before we get to a few happy path automation tests.
This, of course, is far too sensible a strategy ever to happen.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

When should you move API logic out of Next.js? Learn when Route Handlers stop scaling and how ElysiaJS helps.

Explore how Dokploy streamlines app deployment with Docker, automated builds, and simpler infrastructure compared to traditional CI/CD workflows.

A side-by-side look at Astro and Next.js for content-heavy sites, breaking down performance, JavaScript payload, and when each framework actually makes sense.

AI-generated tests can speed up React testing, but they also create hidden risks. Here’s what broke in a real app.re
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