Understanding Cypress synchronous/asynchronous duality
Cypress is a widely used end-to-end testing framework for web applications that offers an easy-to-use API and a powerful command-line interface. It enables developers to write tests that mimic end-users’ interactions with their applications. Understanding how sync/async works is a crucial concept for developers using Cypress.
Some testers argue that this approach can cause unexpected behavior in certain situations and interfere with the application code. Nevertheless, Cypress has implemented this technique to address the problem of flaky tests caused by race conditions and other timing issues. Cypress also provides tools to help developers understand what’s happening under the hood, such as the ability to log and debug the commands being executed.
This code will output:
This code will output:
Understanding Cypress chain of command
Understanding how Cypress handles asynchronous operations is a crucial concept for developers to grasp. Misunderstanding how Cypress deals with asynchronous tasks can create issues and confusion during the debugging process of tests.
Cypress test steps are inherently asynchronous, but the framework includes an engine that enforces the sequential execution of commands. When a Cypress command is invoked, it is not executed immediately but is instead added to a queue for later execution.
To illustrate how Cypress command chaining works using promises, consider the following test example:
Note: there is special logic for handling .should() hence the last line is simplified.
The second code snippet demonstrates how Cypress commands return a promise that can be used to chain together a sequence of commands that are executed in order. This approach ensures that commands are executed synchronously and that the test runner can wait for commands to complete before moving on to the next one.
One thing which complicates the picture even more is Mocha beforeEach webhook. All the commands, including Cypress chain of commands (cy.something()) are guaranteed to execute before tests. It effectively means that Cypress builds a separate chain of command per before, beforeEach, after and afterEach.
Described behaviour has very significant impact on debugging. Consider the following example:
The order of execution is as follows:
- synchronous commands in beforeEach
- Cypress commands (cy.something()) in beforeEach
- synchronous commands in it
- Cypress commands (cy.something()) in_it_
- synchronous commands in_afterEach_
- Cypress commands (cy.something()) in_afterEach_
As you can see for the most effective debugging we have to write code in then() section. However, .then() is a Cypress command, not a Promise. This means you cannot use things like async/await within your Cypress tests.
Cypress offers a low entry level for developers to start testing their web applications, with its easy-to-use API and user-friendly command-line interface. However, this simplicity comes at a cost. The framework is effectively sandboxed, meaning that performing tasks outside of its defined parameters can be difficult. This can limit the ability of developers to fully customize their testing environment and address more complex testing scenarios. Despite its limitations, Cypress provides a solid foundation for web application testing and can be a valuable tool for developers looking to quickly and effectively test their applications. However, it’s important to consider the trade-offs when deciding whether to use Cypress, and to understand its limitations before getting started.
Tags: cypress, test automation, UI testing