As a seasoned Cypress user, I've observed recurring patterns of errors among many engineers over the years. So, this article aims to shed light on 10 common but often overlooked pitfalls in Cypress usage.
Whether you're new to this powerful testing tool or have been using it for some time, you're likely to discover some insightful tips and tricks that haven't been widely discussed before. Get ready to elevate your Cypress skills with some valuable lessons!
1. Using .then()
instead of .should()
for multiple assertions
The .then()
method in Cypress is pivotal for accessing and manipulating the yielded subject within a callback function. This method is particularly useful when you need to perform specific actions or transform values.
However, a common misconception is equating .then()
with .should()
. These methods function differently. The .should()
method is designed to automatically wait and retry until all conditions within the callback function are met. Typically, .should()
passes on the same subject it received from the preceding command. Crucially, any value returned from within a .should()
callback is not utilized, a key difference to remember when writing your Cypress tests.
⚠️ Ensure idempotency in
.should()
callbacksIt's crucial to design callback functions within
.should()
to be resilient and side-effect-free, capable of enduring multiple executions. This is because Cypress employs a retry mechanism for these functions. In case of an assertion failure, Cypress will repeatedly attempt the assertions until a timeout occurs. To adapt to this behavior, your code must be idempotent.
Keep in mind that .and()
is an alias for .should()
. Hence, it waits and retries as well.
❌
cy.get('.header')
.then(($header) => {
expect($header).to.have.length(1)
expect($div[0].className).to.match(/heading-/)
})
✅
cy.get('.header')
.should(($header) => {
expect($header).to.have.length(1)
expect($div[0].className).to.match(/heading-/)
})
2. Explicitly clearing cookies
, local
, and session
storage before each test
Cypress is designed to maintain test integrity by automatically wiping all cookies, local storage, and session storage before each test. This feature is essential for ensuring that no residual state is carried over between tests when test isolation is enabled. Typically, you won't need to manually clear these storages unless you're addressing a specific requirement within a single test, or if test isolation is turned off. Specifically, Cypress takes care of clearing the following before each test:
- cookies across all domains
- local storage across all domains
- session storage across all domains
Understanding this automatic process is crucial for writing effective and isolated tests in Cypress.
❌
before(() => {
cy.clearAllCookies()
cy.clearAllLocalStorage()
cy.clearAllSessionStorage()
});
✅
before(() => {
// perform actions you need
});
3. Passing the --headless
flag
Starting from version 8.0, Cypress has streamlined its operation by defaulting to headless test execution. This enhancement means that the --headless
flag is no longer necessary when you use the cypress run
command. This update simplifies the testing process, making it more efficient and user-friendly. However, it's worth noting that some users still add this flag, perhaps out of habit or for explicit confirmation, even though it's no longer required.
❌
cypress run --browser chrome --headless
✅
cypress run --browser chrome
4. Ignoring experimental features
If you're unfamiliar with experimental features in Cypress, it's essential to start with the official documentation. Staying updated with new releases and reading release notes can give you an edge by acquainting you with the latest experimental features. I highly recommend exploring these notable features, but remember to always refer back to the documentation for the most current information:
-
experimentalMemoryManagement
- enhances memory management for Chromium-based browsers, potentially improving performance and stability. -
experimentalCspAllowList
- allows you to specify which Content-Security-Policy directives are allowed during test runs, offering more control over security settings. -
retries.experimentalStrategy
- by enabling this, you can implement a tailored strategy for test retries based on flake tolerance. Options include detect-flake-but-always-fail or detect-flake-and-pass-on-threshold, providing flexibility in handling test flakiness. -
retries.experimentalOptions
- lets you set specific options for your chosen retries strategy, such as maxRetries, passesRequired, and stopIfAnyPassed, allowing for more nuanced control over test retries.
To use these experimental features, you need to configure them in your cypress.config.js
file. For instance, to enable a particular feature, you would add the relevant configuration entries like this:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
experimentalMemoryManagement: true,
experimentalCspAllowList: true,
retries: {
experimentalStrategy: 'detect-flake-and-pass-on-threshold',
experimentalOptions: {
maxRetries: 2,
passesRequired: 2,
},
openMode: true,
runMode: true,
},
})
⚠️ Remember, experimental features might change or ultimately be removed without making it into the core product. So use them with caution and stay informed about their status and updates.
5. Asserting DOM elements with .should('exist')
A quick GitHub search for the .should('exist')
assertion reveals its presence in over 49,000 files. While some of these instances are legitimately used for non-DOM element assertions, a significant number are applied to DOM commands, where their necessity is debatable.
It's important to note that Cypress inherently waits for DOM commands to become present in the DOM. This automatic waiting mechanism means that explicitly writing .should('exist')
for DOM element verification is often redundant. By understanding this built-in feature of Cypress, you can streamline your tests by omitting unnecessary assertions, leading to cleaner and more efficient tests.
❌
cy.get('button').should('exist').click()
✅
cy.get('button').click()
6. Not setting up ESLint
From my experience, incorporating ESLint in all Cypress projects is not just a best practice, it's essential. However, it's crucial to go beyond just the official Cypress ESLint Plugin. I strongly recommend integrating additional plugins like chai-friendly and mocha plugins to enhance your project's code quality and maintainability.
Starting with the recommended configurations is a wise choice for most projects. But don’t stop there. Dive into the documentation and understand the full potential of these rules. Customize them to suit the specific needs of your project for optimal results. To give you a head start, here's the ESLint configuration file that I've successfully implemented across various projects:
{
"root": true,
"extends": [
"plugin:mocha/recommended",
"plugin:chai-friendly/recommended"
],
"plugins": [
"cypress"
],
"rules": {
"cypress/no-assigning-return-values": "error",
"cypress/no-unnecessary-waiting": "error",
"cypress/no-force": "error",
"cypress/no-async-tests": "error",
"cypress/no-pause": "error",
"cypress/unsafe-to-chain-command": "off",
"mocha/no-mocha-arrows": "off",
"mocha/no-exclusive-tests": "error",
"mocha/prefer-arrow-callback": "error"
},
"env": {
"cypress/globals": true
}
}
💡 It's kind of odd that the official Cypress docs don't make a big deal about setting up ESLint. It's super important for keeping your code clean and making coding a smoother ride. In my last article, I pointed out that ESLint is the top dog in the Cypress world when it comes to downloads from NPM. That just shows how much people rely on it to keep their code in check. So, yeah, setting up ESLint in your Cypress projects? Definitely a good move.
7. Overusing { force: true }
Yes, I know that the { force: true }
is super handy when you just want to skip actionability checks and get things done. But, hear me out – it's really worth it to act like a real user. Doing the actual steps a user would take to interact with your application is key. Plus, when you force things, you might miss some real bugs, because Cypress will breeze past a bunch of functional checks. That's exactly why I've got the cypress/no-force
rule set up in my ESLint config.
❌
cy.get('button').click({ force: true })
✅
cy.get('button').click()
8. Using .then()
to access fixture data and stub response
I've noticed folks using .then()
for all sorts of stuff, but a lot of times, it's just not needed. It's like using a fancy tool when a simple one will do the job. Sure, .then()
can be useful for many things, but when it comes to accessing fixture data or stubbing responses, sometimes keeping it straightforward is the way to go. Let's not overcomplicate things. Less code is better, right?
❌
cy.fixture('users').then((json) => {
cy.intercept('GET', '/users/**', json)
})
✅
cy.intercept('GET', '/users/**', { fixture: 'users' })
9. Avoiding blockHosts
There's this super straightforward config option in Cypress – blockHosts
. I've noticed it's often ignored, but it's actually pretty handy. All you gotta do is add the hosts you want to block to an array in your cypress.config.js
file. Here's how it goes:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
blockHosts: [
"*.google-analytics.com",
"*.codesandbox.io",
"*.loom.com",
"*.youtube.com",
"*.github.com",
"*.googletagmanager.com",
],
})
If you're not sure which host to block, the URL strings and URL objects guide is a great place to start. It'll help you nail down the exact host you're after.
⚠️ Cypress blocks a request to a host you've listed, it automatically whips back a
503
status code. Plus, it slaps ona x-cypress-matched-blocked-host
header, so you can easily tell which rule got hit. Neat, right?
10. Using the wrong Docker image
So, I've been noticing that a lot of projects use the cypress/included
docker image, which comes packed with a bunch of browsers and Cypress pre-installed. But here's the twist – they're not even testing against all those browsers, and they don't really need Cypress installed globally. Let's break down which image you actually need for your tests.
image | system deps | cypress | browsers |
---|---|---|---|
cypress/factory | ✅ | ❗️ (if specified) | ❗️ (if specified) |
cypress/base | ✅ | ❌ | ❌ |
cypress/browsers | ✅ | ❌ | ✅ |
cypress/included | ✅ | ✅ | ✅ |
Most of the time, you just need the image with Node, system dependencies, and the browser you're testing with. No need to pull all the browsers. Like, if you're only testing in Chrome, cypress/browsers
is your go-to. Want to build a custom image? cypress/factory
is great for picking the exact versions you want. If you've got browsers installed already or want to add them yourself, cypress/base
is all about those system dependencies. And if you're into the whole all-inclusive vibe, cypress/included
has got you covered.
Conclusion
Cypress is a great and powerful framework for testing, but it's the little things that count. Remember the insights on .then()
vs .should()
, using the right Docker image, and setting up ESLint. Avoid common traps like overusing { force: true }
and unnecessary assertions. This article aimed to give you the lowdown on these key details to sharpen your Cypress skills.
Hope you found some helpful nuggets here. Happy testing with Cypress!
Top comments (0)