More Cypress Best Practices
Tags: testing, cypress, javascript
Published: 2023-04-02 14:24:55

These are some additional Cypress best practices that I haven't seen in any other articles about Cypress.

## Use [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress)

The best way to avoid anti-patterns is to have a tool that will catch them for you as early as possible.

Make sure to enable all available rules:

```json
{
  "extends": [
    "plugin:cypress/recommended"
  ],
  "rules": {
    "cypress/no-force": "error",
    "cypress/assertion-before-screenshot": "error",
    "cypress/require-data-selectors": "error",
    "cypress/no-pause": "error",
  }
}
```

And make sure to read about each rule to improve your understanding of Cypress.

## Use UI assertions to reduce test flakiness

If you find that a certain action sometimes gets performed earlier than it should, then add a UI assertion before it.

In this example we want to wait for a comment to appear before adding another one, otherwise, we may end up with a flaky race condition:

```diff
  cy.findByRole("textbox", { name: "Comment" }).type("My first comment");
  cy.findByRole("button", { name: "Post comment" }).click();
+ cy.findByText("My first comment").to("be.visible");
  cy.findByRole("textbox", { name: "Comment" }).type("My second comment");
  cy.findByRole("button", { name: "Post comment" }).click();
```

## Use UI assertions to reduce screenshot flakiness

This is what [`cypress/assertion-before-screenshot`](https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/assertion-before-screenshot.md) rule is about. If you take screenshots without assertions then you may get different screenshots depending on timing.

Unfortunately, the rule works only with the default `cy.screenshot()` command, so you need to be diligent if you use a different one, like `cy.percySnapshot()`.

In this example, after publishing a new post, we want to wait for it to appear in the UI before taking a screenshot:

```diff
  cy.findByRole("button", { name: "Publish" }).click();
+ cy.findByRole("heading", { name: "My new post" }).to("be.visible");
  cy.percySnapshot("Posts - new post");
```

## Use functions to create reusable commands

In most cases, performing a single meaningful action will involve executing multiple commands and assertions. If you find yourself repeating the same commands over and over, create a function that abstracts them away.

```ts
export function goToPosts() {
  cy.findByAltText("Menu").trigger("mouseover");
  cy.findByRole("link", { name: "Posts" }).click();
}

export function createPost(name: string, content: string) {
  cy.findByRole("textbox", { name: "Post name" }).type(name);
  cy.findByRole("textbox", { name: "Post content" }).type(content);
  cy.findByRole("button", { name: "Publish" }).click();
  cy.findByRole("heading", { name }).to("be.visible");
}

export function addComment(comment: string) {
  cy.findByRole("textbox", { name: "Comment" }).type(comment);
  cy.findByRole("button", { name: "Post comment" }).click();
  cy.findByText(comment).to("be.visible");
}

export function deletePost(postName: string) {
  cy.findByRole("button", { name: `Open ${postName} menu` }).click();
  cy.findByRole("button", { name: `Delete ${postName}` }).click();
  cy.findByRole("button", { name: "Yes" }).click();
}
```

It will make your tests concise and clean:

```js
describe("Posts", () => {
  it("can create a new post", () => {
    const postName = "My new post";

    goToPosts();

    createPost(postName, "My post content");

    addComment("My first comment");
    addComment("My second comment");

    cy.findByText("My second comment").to("be.visible");
    cy.screenshot();

    deletePost(postName);
  });
});
```

## Make production code more accessible

If you find that it's impossible or hard to use an accessible [Testing Library](https://testing-library.com/docs/cypress-testing-library/intro/) query, go ahead and change the production code to make it more accessible. It will make your users happier, and your tests less dependent on implementation details.

```diff
- <div onClick={handleClick}>
+ <button type="button" onClick={handleClick}>
    Create post
- </div>
+ </button>
```

## Use `cy.within()` instead of `cy.findAllBy*()`

If a page contains multiple elements with the same role and name (like multiple forms with "Save" button), instead of using `cy.findAllBy*().first()` or `cy.findAllBy*().eq(<index>)`, try to use `cy.within()`. It will make your tests less dependent on implementation details. But better yet, try to avoid those situations in the first place.

```diff
- cy.findAllByRole("button", { name: "Save" }).eq(1).click();
+ cy.findByRole("form", { name: "User settings" }).within(() => {
+   cy.findByRole("button", { name: "Save" }).click();
+ });
```

## Use [cypress-plugin-steps](https://github.com/filiphric/cypress-plugin-steps)

If you find yourself using code comments a lot in your Cypress tests, consider using `cy.step()` from cypress-plugin-steps instead. It will show the messages in the test log, and if your test fails, your scenario will be added to the error message, and it will be easier to pinpoint the failed command.

```diff
- // Go to posts
+ cy.step("Go to posts");
  cy.findByAltText("Menu").trigger("mouseover");
  cy.findByRole("link", { name: "Posts" }).click();

- // Delete post
+ cy.step("Delete post");
  cy.findByRole("button", { name: 'Open "My new post" menu' }).click();
  cy.findByRole("button", { name: 'Delete "My new post"' }).click();
  cy.findByRole("button", { name: "Yes" }).click();
```

---

If you liked this article, you should check out
[**Cypress Best Practices that are actually bad**](https://dev.to/evgenyorekhov/cypress-best-practices-that-are-actually-bad-2m3m)