Cypress Asynchronous Nature

註明本文轉載於:https://www.toolsqa.com/cypress/cypress-asynchronous-nature/

Cypress Asynchronous Nature

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 is a Javascript end to end testing framework and uses multiple features of asynchronous JavaScript such as Promisecallbacks, 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 steeps 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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

// 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('#menu-top > li:nth-child(3) > a').click();

 

    //Verify number of items present on Widget Tab

    cy.get('.demo-frame > ul > li').should('have.length',19);

 

    //Print a string to console to show Async nature

    console.log('XXXX')

 

    //Verify number of items having keyboard as text on Widgets Tab

    //Get and Find Command (Parent - Child Relationship)

 

    cy.get('.demo-frame > ul > li').find('[href*=keyboard]').should('have.length',2);

    

  })

})

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:

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:

  1. Open the URL “https://www.demoqa.com/.”
  2. Click on the Widgets Menu Item
  3. Validate the number of items returned on Widgets Tab
  4. Validate the number of items having a “keyboard” in the “href” attribute.

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 articles returned on 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 behavior/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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// 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('#menu-top > li:nth-child(3) > a')

            })

            .then(($element) => {

                return cy.click($element)

            })

          

            .then(() => {

                //Length Assertions

                cy.get('.demo-frame > ul > li').should('have.length',19);

            })

    })

})

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 behaviors of promises.

 

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

describe('Search Test Suite', function () {

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

 

    //Visit the Demo QA Website

    cy.visit("https://demoqa.com/");

    

   // Clicking on Widget Menu Item

    

   cy.get('#menu-top > li:nth-child(3) > a').click();

 

    //Verify number of items present on Widget Tab

    cy.get('.demo-frame > ul > li').should('have.length',19);

    })

})

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 behavior for each test run, Cypress doesn’t provide the functionality to run commands in parallel. Which, in turn, is a very common behavior 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 Cypress Asynchronous Nature and its promise handling. In the next article, let’s learn about how to handle “Non-Cypress Async Promises“.

Category: CypressBy Aashish KhetarpalApril 4, 2020

註明本文轉載於:https://www.toolsqa.com/cypress/cypress-asynchronous-nature/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章