DEV Community

loading...
Webiny

5 Things to Avoid When Writing Cypress Tests

Swapnil M Mane
Originally published at webiny.com ・5 min read

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"
/>
Enter fullscreen mode Exit fullscreen mode

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

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

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
    });

Enter fullscreen mode Exit fullscreen mode

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

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

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", {
        // ...
    });
});
Enter fullscreen mode Exit fullscreen mode

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

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

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.

Discussion (0)