DEV Community

Evgeny Orekhov
Evgeny Orekhov

Posted on • Edited on

More Cypress Best Practices

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

Use 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:

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

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:

  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();
Enter fullscreen mode Exit fullscreen mode

Use UI assertions to reduce screenshot flakiness

This is what cypress/assertion-before-screenshot 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:

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

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.

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();
}
Enter fullscreen mode Exit fullscreen mode

It will make your tests concise and clean:

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

Make production code more accessible

If you find that it's impossible or hard to use an accessible Testing Library 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.

- <div onClick={handleClick}>
+ <button type="button" onClick={handleClick}>
    Create post
- </div>
+ </button>
Enter fullscreen mode Exit fullscreen mode

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.

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

Use 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.

- // 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();
Enter fullscreen mode Exit fullscreen mode

If you liked this article, you should check out
Cypress Best Practices that are actually bad

Top comments (0)