5 Things to Avoid When Writing Cypress Tests

AshutoshTwitter
July 26, 2021

When it comes to testing an application, End-to-End (E2E) testing provides the most confidence and the most bang for the buck.

Testing Pyramid

On the contrary, there is no doubt that End-to-End testing is hard, time-consuming, and comes with a bag of issues to solve. But, only if you're using the wrong tool for the job.

Enter Cypress: Fast, easy, and reliable testing for anything that runs in a browser.

Cypress helps in solving most of the pain points of End-to-End testing and makes it fun to write tests. But, there are certain mistakes to be avoided so that you can get the full benefit of working with Cypress.

In this blog post, we'll cover 5 such common mistakes, which should be avoided when writing Cypress tests. So, without further ado, let's begin!

Use id and class For Selecting Element

Using id and class for selecting element is problematic because they are primarily for behavior and styling purposes, due to which they are subject to change frequently. Doing so result in brittle tests which, you probably don't want.

Instead, you should always try to use data-cy or data-test-id. Why? Because they are specifically for testing purposes, which makes them decoupled to the behavior or styling, hence more reliable.

For example, let's suppose we have an input element:

<input
  id="main"
  type="text"
  class="input-box"
  name="name"
  data-testid="name"
/>

Instead of using id or class to target this element for test, use data-testid:

// Don't ❌
cy.get("#main").something();
cy.get(".input-box").something();

// Do ☑️
cy.get("[data-testid=name]").something();

What about using text for selecting element?

Sometimes it is necessary to use text such as button label to make an assertion or action. Although, it's perfectly fine, keep in mind that, your test will fail if the text changes, which is what you might want if the text is critical for the application.

Treating Cypress Commands as Promise

Cypress tests are composed of Cypress commands, for example, cy.get and cy.visit. Cypress commands are like Promise, but they are not real Promise.

What that means is, we can't use syntax like async-await while working with them. For example:

    // This won't work
    const element = await cy.get("[data-testid=element]");

    // Do something with element

If you need to do something after a command has been completed, you can do so with the help of the cy.then command. It will guarantee that only after the previous command finishes, the next will run.

    // This works
    cy.get("[data-testid=element]").then($el => {
        // Do something with $el
    });

Note when using a clause like Promise.all with Cypress command, it might not work as you expect because Cypress commands are like Promise, but not real Promise.

Using Arbitrary Waits in Cypress Tests

When writing the Cypress test we want to mimic the behavior of a real user in real-world scenarios. Real-world applications are asynchronous and slow due to things like network latency and device limitations.

When writing tests for such applications we are tempted to use arbitrary values in the cy.wait command. The problem with this approach is that, while it works fine in development, it is not guaranteed. Why? Because the underlying system depends upon things like network requests which are asynchronous and nearly impossible to predict.

    // Might work (sometimes) 🤷
    cy.get("[data-testid=element]").performSomeAsyncAction();
    // Wait for 1000 ms
    cy.wait(1000);
    // Do something else after the action is completed

Instead, we should wait for visual elements, for example, completion of loading. Not only does it mimic the real-world use case more closely, but it also gives more reliable results. Think about it, a user using your application mostly likely wait for a visual clue like loading to determine the completion of an action rather than arbitrary time.

    // The right way ☑️
    cy.get("[data-testid=element]").performSomeAsyncAction();
    // Wait for loading to finish
    cy.get("[data-testid=loader]").should("not.be.visible");
    // Now that we know previous action has been completed; move ahead

Cypress commands, for example, cy.get wait for the element before making the assertion, of course for a predefined timeout value which you can modify. The cool thing about timeout is that they will only wait until the condition is met rather than waiting for the complete duration like the cy.wait command.

Using Different Domains within a Cypress Test

One limitation of Cypress is that it doesn't allow using more than one domain name in a single test.

If you try using more than one domain in a single test block it(...) or test(...), Cypress will throw a security warning. This is the way Cypress has been built.

With that being said, sometimes there is a requirement to visit more than one domain in a single test. We can do so by splitting our test logic into multiple test blocks within a single test file. You can think of it as a multi-step test, for example,

describe("Test Page Builder", () => {
    it("Step 1: Visit Admin app and do something", {
        // ...
    });

    it("Step 2: Visit Website app and assert something", {
        // ...
    });
});

We use a similar approach at Webiny for testing the Page Builder application.

Few things to keep in mind when writing tests in such a manner are:

  1. You cannot rely on persistent storage be it variable in test block or even local storage. Why? Because, when we issue a Cypress command with a domain other than the baseURL defined in the configuration, Cypress performs a tear-down and does a full reload.

  2. Blocks like "before", "after" will be run for each such test block because of the same issue mentioned above.

Be mindful of these issues before adapting this approach and adjust the tests accordingly.

Mixing Async and Sync Code

Cypress commands are asynchronous, and they don't return a value but yield it.

When we run Cypress it won't execute the commands immediately but read them serially and queue them. Only after it executes them one by one. So, if you write your tests mixing async and sync code, you will get the wrong results. For example:

it("does not work as we expect", () => {
  cy.visit("your-application") // Nothing happens yet

  cy.get("[data-testid=submit]") // Still nothing happening
    .click() // Nope, nothing

  // Something synchronous
  let el = Cypress.$("title") // evaluates immediately as []

  if (el.length) {
    // It will never run because "el.length" will immediately evaluates as 0
    cy.get(".another-selector")
  } else {
    /*
    * This code block will always run because "el.length" is 0 when the code executes
    */
    cy.get(".optional-selector")
  }
})

Instead, use our good friend cy.then command to run code after the command has been completed. For example,

it("does work as we expect", () => {
  cy.visit("your-application") // Nothing happens yet

  cy.get("[data-testid=submit]") // Still nothing happening
    .click() // Nope, nothing
    .then(() => {
      // placing this code inside the .then() ensures
      // it runs after the cypress commands 'execute'
      let el = Cypress.$(".new-el") // evaluates after .then()

      if (el.length) {
        cy.get(".another-selector")
      } else {
        cy.get(".optional-selector")
      }
    })
})

Conclusion

Cypress is a powerful tool for End-to-End testing, but sometimes we make few mistakes which makes the experience not fun. By avoiding the common mistakes we can make the journey of End-to-End testing smooth and fun.


Thanks for reading! My name is Ashutosh and I work as a full stack developer at Webiny. If you have any questions, comments or just want to connect, feel free to reach out to me via Twitter.

Find more articles on the topic of:CypressEnd-to-End testingWeb developmentOpen source

About Webiny

Webiny is an open-source framework that helps developers and organizations to build applications that run on top of the serverless infrastructure.

Learn More

Newsletter

Want to get more great articles like this one in your inbox. We only send one newsletter a week, don't spam, nor share your data with 3rd parties.

Webiny free to use and released under the MIT open source license.
GitHub / Twitter / YouTube / Slack / Blog
Webiny Ltd © 2021

Email
  • We send one newsletter a week.
  • Contains only Webiny relevant content.
  • Your email is not shared with any 3rd parties.
By using this website you agree to our privacy policy
Webiny Chat

Find us on Slack

Webiny Community Slack