Understanding the Asynchronous Nature of Cypress
Cypress, the popular end-to-end testing framework, empowers developers to create robust and reliable test suites for web applications. However, grasping the asynchronous nature of Cypress commands is essential for harnessing its full potential. Misunderstanding this aspect can lead to issues during test execution and debugging. In this article, we’ll delve into the asynchronous behavior of Cypress and how to effectively work with it.
Return vs. Yield
A fundamental concept to grasp in Cypress is that its commands do not return their subjects. This means you cannot directly assign the result of a Cypress command to a variable for subsequent use. For instance, the following code won’t work as expected:
// THIS WILL NOT WORK
const button = cy.get("button")
button.click()
Instead, Cypress commands yield their subjects. They are asynchronous and queued for execution at a later time. This queuing allows Cypress to perform necessary tasks between commands to ensure proper sequencing and handling of asynchronous operations.
.then()
To interact with the subject directly, Cypress provides the .then() method. Similar to Promises in JavaScript, .then() allows you to perform actions on the yielded subject. However, it's crucial to note that .then() is a Cypress command, not a Promise. Therefore, you cannot use async/await within Cypress tests.
cy.get("button").then(($btn) => {
const cls = $btn.attr("class")
// Perform some action
$btn.click()
// Assertion
cy.wrap($btn).should("not.have.class", cls)
})
Within the .then() callback, you can manipulate the subject and perform assertions. Here, after extracting the button element $btn, we perform a click action on it and then assert that it no longer has the class cls.
.wrap()
When working with subjects like jQuery objects, you need to use cy.wrap() to provide Cypress with the proper context for interaction. For example:
cy.get("button").then(($btn) => {
const cls = $btn.attr("class")
// Wrap the button for further interaction
cy.wrap($btn).click().should("not.have.class", cls)
})
Here, cy.wrap() is used to wrap the jQuery object $btn before performing actions like .click() and assertions like .should().
Commands Are Asynchronous
It is very important to understand that Cypress commands don’t do anything at the moment they are invoked, but rather enqueue themselves to be run later. This is what we mean when we say Cypress commands are asynchronous.
Take this short test, for example:
// End-to-End Test
it('hides the thing when it is clicked', () => {
cy.visit('/my/resource/path') // Nothing happens yet
cy.get(".hides-when-clicked") // Still nothing happening
.should("be.visible") // Still absolutely nothing
.click() // Nope, nothing
cy.get('.hides-when-clicked') // Still nothing happening
.should('not.be.visible') // Definitely nothing happening yet
})
// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!
Cypress doesn’t kick off the browser automation until the test function exits.
Mixing Async and Sync Code
Remembering that Cypress commands run asynchronously is important if you are attempting to mix Cypress commands with synchronous code. Synchronous code will execute immediately — not waiting for the Cypress commands above it to execute.
领英推荐
Incorrect Usage
In the example below, the el evaluates immediately, before the cy.visit() has executed, so it will always evaluate to an empty array.
it('does not work as we expect', () => {
cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // Still nothing happening
.click() // Nope, nothing
// Cypress.$ is synchronous, so evaluates immediately
// there is no element to find yet because
// the cy.visit() was only queued to visit
// and did not actually visit the application
let el = Cypress.$('.new-el') // evaluates immediately as []
if (el.length) {
// evaluates immediately as 0
cy.get('.another-selector')
} else {
// this will always run
// because the 'el.length' is 0
// when the code executes
cy.get('.optional-selector')
}
})
Correct Usage
Below is one way the code above could be rewritten in order to ensure the commands run as expected.
it('does not work as we expect', () => {
cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // 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')
}
})
})
Incorrect Usage
In the example below, the check on the username value gets evaluated immediately, before the cy.visit() has executed, so it will always evaluate to undefined.
it('test', () => {
let username = undefined // Evaluates immediately as undefined
cy.visit('https://example.cypress.io') // Nothing happens yet
cy.get('.user-name') // Still, nothing happens yet
.then(($el) => {
// Nothing happens yet
// This line evaluates after the .then executes
username = $el.text()
})
// This evaluates before the .then() above
// So the username is still undefined
if (username) {
// Evaluates immediately as undefined
cy.contains(username).click()
} else {
// This will always run
// Because username will always
// Evaluate to undefined
cy.contains('My Profile').click()
}
})
Correct Usage
Below is one way the code above could be rewritten in order to ensure the commands run as expected.
it('test', () => {
let username = undefined // Evaluates immediately as undefined
cy.visit('https://example.cypress.io') // Nothing happens yet
cy.get('.user-name') // Still, nothing happens yet
.then(($el) => {
// Nothing happens yet
// This line evaluates after the .then() executes
username = $el.text()
// Evaluates after the .then() executes
// It's the correct value gotten from the $el.text()
if (username) {
cy.contains(username).click()
} else {
cy.get('My Profile').click()
}
})
})
// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!
Core Concept
It’s essential to understand that each Cypress command and chain of commands returns immediately, queuing them for later execution. This design ensures deterministic and controlled execution, vital for handling the mutable nature of the DOM and preventing flakiness in tests.
Why Can’t I Use async/await?
While modern JavaScript developers might be accustomed to using async/await for handling asynchronous operations, Cypress’s design patterns are intentionally different. This distinction ensures reliable and predictable test execution in the context of web applications.
Using .then() to Act on a Subject
By adding .then() to a command chain, you can directly access the yielded subject. If you wish to continue chaining commands after .then(), ensure to return a value other than null or undefined to propagate the subject to subsequent commands.
In conclusion, understanding Cypress’s asynchronous nature is paramount for writing effective and reliable tests. By embracing its unique approach and mastering concepts like .then() and cy.wrap(), developers can build robust test suites that accurately reflect the behavior of their web applications.