10 Mistakes to Avoid When Using Cypress
Tags: cypress, javascript, testing, tutorial
Published: 2024-01-19 11:41:15

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()` callbacks

> It'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.

```javascript
❌
cy.get('.header')
  .then(($header) => {
    expect($header).to.have.length(1)
    expect($div[0].className).to.match(/heading-/)
  })
```

```javascript
✅
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.

```javascript
❌
before(() => {
  cy.clearAllCookies()
  cy.clearAllLocalStorage()
  cy.clearAllSessionStorage()
});
```

```javascript
✅
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**.

```bash
❌
cypress run --browser chrome --headless
```

```bash
✅
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](https://docs.cypress.io/guides/references/experiments). 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: 

```javascript
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**.

![github search](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wdeog7zs5zq7i76bgl2s.png)

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.

```javascript
❌
cy.get('button').should('exist').click()
```

```javascript
✅
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](https://github.com/cypress-io/eslint-plugin-cypress/pulls). I strongly recommend integrating additional plugins like [chai-friendly](https://www.npmjs.com/package/eslint-plugin-chai-friendly) and [mocha](https://github.com/lo1tuma/eslint-plugin-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:

```json
{
  "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](https://hackernoon.com/the-most-popular-cypress-packages-in-2023), 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.

```javascript
❌
cy.get('button').click({ force: true })
```

```javascript
✅
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?

```javascript
❌
cy.fixture('users').then((json) => {
  cy.intercept('GET', '/users/**', json)
})
```

```javascript
✅
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:

```javascript
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](https://nodejs.org/api/url.html#url_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 on `a x-cypress-matched-blocked-host` header, so you can easily tell which rule got hit. Neat, right?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jo2n9cpwzeu4u2r79g54.png)

## 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!

**Follow me on [GitHub](https://github.com/elaichenkov) and [Twitter](https://twitter.com/elaichenkov).**