Enroll in Selenium Training

Asynchronous programming is a means of parallel programming whereby, a unit of work runs separately from the main application thread. Additionally, it notifies the calling thread of its completion, failure, or progress. These types of programs are "non-blocking".  When you execute something synchronously, you wait for it to finish before moving on to another task. On the other hand, when you run something asynchronously, you can move on to another task before it ends.

To detail it further, if we run a synchronous program, every step will execute sequentially, and the next steps will perform only when the previous step has completed its execution and have returned the expected result. But if you are running an asynchronous program, the different steps will not have a dependency on each other. Even though the program writing is sequential, it will not wait for any step to complete, and it will execute every step and won't worry about the state/output of the previous step. It just runs all the steps which are available for execution. The below image beautifully depicts the difference between a Synchronous and Asynchronous call to a server from a client.

Cypress Asynchronous vs Synchronous

Cypress is a Javascript end to end testing framework and uses multiple features of asynchronous JavaScript such as Promise, callbacks, etc. which makes Cypress commands as asynchronous as well. Still, there are specific means which we can utilize to control the test execution and make it behave like synchronous execution. In this article, we will be covering the following topics to check and validate the asynchronous nature of Cypress:

  • Cypress commands are Asynchronous
  • These commands run serially
  • Cypress commands are promises
  • These commands are not promises

Cypress commands are Asynchronous

All the Cypress test steps are asynchronous, but Cypress has an engine in itself (wrapper in the backend) which makes sure that our test execution of cypress commands should happen in sequence, i.e., all the Cypress commands don't do anything at the moment they are invoked, they just enqueue themselves, so as can be run later on. Each Cypress command returns immediately, which appends to a queue of commands which execute at a later time. But when we use Cypress commands along with Javascript commands, it becomes asynchronous.

To understand the concept further, save the following code as cypressTest3.js under the examples folder:

// type definitions for Cypress object "cy"
/// <reference types="cypress" />
 
describe('My First Cypress Test', function() {
  it('Visits the ToolsQA Demo Page and check the menu items', function() {
  //Visit the Demo QA Website
  cy.visit("https://demoqa.com/");
  
 // Clicking on Widget Menu Item
  cy.get(':nth-child(4) > :nth-child(1) > .avatar > svg').click();

  //Verify number of items present under the Widget Tab
  cy.get(':nth-child(4) > .element-list > .menu-list > li').should('have.length',9);

  //Print a string to console to show Async nature 
  console.log('XXXX')

  //Verify the number of span items under the Widgets Tab
  cy.get(':nth-child(4) > .element-list > .menu-list > li').find('span').should('have.length',9);
  
})
})

In the above code, we have added a new line, which just prints the value "XXXX" to browser's Console, and we are doing this after clicking on the Widgets icon.

Now run the test case, and before running, open the browser console by pressing F12 inside the Cypress Test runner or by right-clicking on the right-side page, which displays the web-page.

The sample output of the test run appears as below:

Visualising Cypress Synchronous Nature

The above figure makes it clear that even though Cypress is still on execution for opening the Widgets page, as shown on the left panel. Moreover, the Console has already printed the value "XXXX", which confirms that steps in the test case don't wait for each other to complete and behave asynchronously. Still, as we discussed above, the cypress commands enqueue themselves. However, the question arises, why console.log command did not enqueue? The reason for the same is that console.log is not cypress command, and this is the reason it did not enqueue. So if there will be any JavaScript commands in the tests, they will not wait for the Cypress commands to complete their tasks and will continue their execution.

Cypress commands run serially

Even though Cypress is a Javascript framework whose commands are asynchronous, but still all the Cypress commands run serially. After a test function finishes running, Cypress starts executing the commands that enqueued using the "cy." command chains. So ideally, the test mentioned above will execute in the following order:

  • Open the URL "https://www.demoqa.com/".
  • Click on the Widgets Tab
  • Validate the number of items under the Widgets tab
  • Validate the number of span items under the Widgets tab.

From the test execution, it is clear to use that all these actions happen serially, so how come Cypress ensures for this serial execution even though it claims that all the Cypress commands are asynchronous. In actual there is magic happening beside the execution, using which Cypress ensures the serial execution of all these commands. Lets again revisit the above steps with the hidden commands that are executed by Cypress to ensure that the execution happens serially:

  1. Open the URL "https://www.demoqa.com/".

    • and wait for the page load event to fire after all external resources have loaded.
  2. Click on the Widgets Menu Item

    • and wait for the element to reach an actionable state or, in other words, a new page load event to fire.
  3. Validate the number of items returned under the Widgets tab and same is for step 4 execution

    • and retry until the assertion passes and command times out.

As you can see, Cypress puts in lots of effort to ensure the state of the application matches what our commands expect about it. Any waiting or retrying that is necessary to ensure a step was successful must complete before the next phase begins. The test will fail if they don’t complete successfully before the timeout reaches.

Cypress commands are Promises

As stated above, Cypress enqueues all the commands before starting the execution of them. We can rephrase it by saying that "Cypress enqueues promises to the chain of promises". Promises in the programming language are almost the same as promises in general language on humans. A promise is a state which depicts the behaviour/action of the person. Depending on the situation, nature, and conditions, a person can either fulfill or deny the Promise. When the Promise happens, it was in an indeterministic state which can either resolve to fulfill or a denied state.

On the same, Promise in case of Cypress is a state of the command, which will have the following states:

  • Pending: When step execution starts, and the result is pending.
  • Rejection: It happens when we get any failure for any step.
  • Resolved: When the step successfully executes.

So, how a typical javaScript user will write the above code to ensure that the next command executes only after the previous command as returned its response:

// This is not an actual code, the "then" statements are being added 
// just for demontration purpose

describe('Search Test Suite', function () {
    it('Search Cucumber Test Case', function () {

        cy.visit("https://www.demoqa.com/")
            .then(() => {
                return cy.get(':nth-child(4) > :nth-child(1) > .avatar > svg')
            })
            .then(($element) => {
                return cy.click($element)
            })
           
            .then(() => {
                //Length Assertions
                cy.get(':nth-child(4) > .element-list > .menu-list > li').should('have.length',9);
            })
    })
})

Doesn't this code look very clumsy? Cypress handles all this internally. All the promises are wrapped-up and hidden from the user. In addition to the code readability, Cypress ensures the retry-ability, which is not the default behaviours of promises.

We can easily accomplish all the expected steps in the above code with a minimal and better readable code as below:

describe('Search Test Suite', function () {
    it('Search Items under Widgets Tab', function () {

    //Visit the Demo QA Website
    cy.visit("https://demoqa.com/");
    
   // Clicking on Widget Menu Item
    
   cy.get(':nth-child(4) > :nth-child(1) > .avatar > svg').click();

    //Verify number of items present on Widget Tab
    cy.get(':nth-child(4) > .element-list > .menu-list > li').should('have.length',9);
    })
})

In actual, Cypress commands don't return typical Promise instances. Instead, it returns a Chain that acts like a layer sitting on top of the internal Promise instances. So, even though Cypress commands are like promises, but Cypress itself ensures that the next step will execute only when the previous command/promise has resolved to a state.

Cypress Promise Exceptions

Even though Cypress APIs have a Promise like qualities, it is not an exact 1:1 implementation of Promises. Following are the significant points which differentiate the Cypress commands from Promises:

  • Parallel execution of Cypress commands is not feasible: As Cypress executes all its commands serially and ensures that it provides the same consistent behaviour for each test run, Cypress doesn't provide the functionality to run commands in parallel. Which, in turn, is a very common behaviour for the Promises.
  • No chance to forget the return of a Promise: In real Promises, it’s very easy to ‘lose’ a nested Promise if you don’t return it or chain it incorrectly. Cypress enqueue all commands onto a global singleton, which ensures that no commands will ever be lost.
  • Can't add ".catch" error handler for failed commands: Cypress doesn't support built-in error recovery from a failed command. A command and its assertions all eventually pass, and if one fails, all remaining commands are not run and lead to the failure of the test.

Key Takeaways

  • Cypress commands are Asynchronous. Moreover, they return just by queueing the command and not waiting for the completion of commands.
  • Even being Asynchronous, all the Cypress commands execute serially with the help of Cypress Engine.
  • Cypress commands are similar to Promises, which can pass and fail depending on the promisee resolution.
  • Cypress commands are not complete Promises, as they can't run in parallel and can't have explicit exception handling with ".catch()" handler.

So this was all about i and its promise handling. In the next article, let’s learn about how to handle "Non-Cypress Async Promises".

If you want to learn more you can look at this video series: ++Cypress video series++

Cypress Get Command
Cypress Get Command
Previous Article
Handle Non-Cypress Async Promises
Handle Non-Cypress Async Promises
Next Article

Similar Articles

Feedback