visit
Enter : 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!id
and class
For Selecting ElementUsing 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.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.
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 . 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.
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 .
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 .
Few things to keep in mind when writing tests in such a manner are:
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 , Cypress performs a tear-down and does a full reload.
Blocks like "before"
, "after"
will be run for each such test block because of the same issue mentioned above.
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")
}
})
})