End-to-end Testing
How-tos & Guides
6 min read

Strategies for handling async code in Cypress

Learn how to factor Cypress tests to handle asynchronous behavior within the application under test.

Priscy Ikechi
Published August 2, 2022

It can be difficult to determine what is executing synchronously vs asynchronously within a web application. Static assets in the page, such as images, CSS, and other media, are requested in order but can be processed either synchronously or asynchronously depending on the type of asset it is, and how it’s defined within the page. Cascading style sheets (CSS) are evaluated synchronously by the browser, but images, fonts, and videos referenced inside the CSS are loaded and display asynchronously. Meanwhile, JavaScript exposes a single-threaded, asynchronous programming model to the developer, but is executed in the order it appears on the page unless otherwise specified by the developer.

In this article we’ll cover strategies for testing the various synchronous and asynchronous operations in a browser using the Cypress testing framework.

Cypress and asynchronously

Cypress tests run on the same event loop that is used by the underlying applications. This means all Cypress tests operate asynchronously. Cypress has logic that ensures that commands execute in sequence when those operations are occurring asynchronously.

Consider the following simple Cypress test:

describe("Task tracker", () => {
  it("loads successfully", () => {
    cy.visit("http://localhost:3000");

    cy.get(":nth-child(2) > h3").click();
    cy.get(":nth-child(1) > input").type("add a test");

    // Logs the string to the console in order to show Cypress Asynchronous behavior
    console.log("Cypress is Asynchronous");

    // Click on the button element
    cy.get(":nth-child(1) > button").click();
  });
});

In the code above, we added a line that prints the value “Cypress is Asynchronous” to the browser console. It appears that this console log statement would execute after we click the h3 element and an input box.

However, when opening the browser console and running the code above, you’ll observe that the value “Cypress is Asynchronous” is printed to the browser console before the click on the h3 element and input box.

Why is this happening? The reason is due to the asynchronous nature of Cypress. The .get() call simply enqueues the command onto the main event loop, and the actual execution of that command occurs asynchronously when the enqueued event is handled. Once the command is enqueued, operation proceeds to the new line of code, which is why the console.log() statement is printed before the actions ever occur.

If we want the above command to be carried out in the correct order, then the console.log() should be moved inside a then() callback:

describe("Task tracker", () => {
  it("loads successfully", () => {
    cy.visit("http://localhost:3000");

    cy.url().should("include", "/technology");

    cy.get(":nth-child(2) > h3").click();
    cy.get(":nth-child(1) > input")
      .type("add a test")
      .then(() => {
        console.log("Cypress is Asynchronous");
      });

    // Click on the button element
    cy.get(":nth-child(1) > button").click();
  });
});

Now when we run the test, we don’t see anything printed in the browser console until after the h3 and input box interactions have executed.

Cypress and Promises

Promises are used to handle asynchronous operations in JavaScript. They are simple to manage, even when dealing with multiple asynchronous operations.

Let’s take a look at another example:

describe('Task tracker', () => {
  it('loads successfully', () => {
    cy.visit('http://localhost:3000');

    cy.get(':nth-child(2) > h3')
      .then(() => {
        return cy.click();
      })

    cy.window()
      .its('navigator.clipboard')
      .invoke('readText')
      .then(() => {
        return cy.should('include', '/technology');
      });
    });
  });
});

Each cy command returns a Promise, and we define the .then() callback in each promise to indicate what code should be executed if the Cypress command completes successfully.

One nice feature of Cypress is that the Cypress runtime has built-in logic to retry failed operations. If, for example, an element is not immediately present on a page, it will retry the action for a certain amount of time before considering the action failed. This built-in behavior can help make tests more resilient to false failures.

The above code can be succinctly written without including promises manually, as shown below:

describe('Task tracker', () => {
  it('loads successfully', () => {
    cy.visit('http://localhost:3000');

    cy.get(':nth-child(2) > h3').click();

    cy.window()
      .its('navigator.clipboard')
      .invoke('readText')
      .should('include', '/technology')
    });
  });
});

One major limitation of Cypress’s async behavior is that you can’t use the async / await constructs that are native to JavaScript. This is unfortunate because async / await helps async code be more readable and look more like synchronous code, and for these reasons is often preferred to Promise callbacks by JavaScript developers.

Strategies for handling async code in Cypress

In this section, we will look at various ways of handling asynchronous code while testing with Cypress. As we covered in our Regression Testing Guide, having a proper testing strategy for dealing with asynchronous behavior is crucial to preventing flaky tests. Below we’ll cover some best practices for dealing with this in your Cypress tests.

Avoid using cy.wait to wait for number of milliseconds

cy.wait() is used to wait for a set number of milliseconds for an element to be resolved before moving on to the next command. Many developers use cy.wait() to code hard-coded waits in their tests, perhaps to wait for a network call or some other operation to complete. Using cy.wait() for this purpose should be considered a bad practice; at best your tests will be waiting around doing nothing, and at worst your test could be not waiting long enough for it’s dependent async action to complete.

Using cy.intercept to handle network requests

There may be circumstances where you want a network request to complete before taking the next action in a test. A more robust alternative to hard-coded waits is cy.intercept(). This method can be used intercept and wait for network requests to complete. This is a very powerful strategy, since it tells the test to wait just long enough for the asynchronous operation to complete, which keeps tests fast and helps them be more reliable.

Below is an example of how to use cy.intercept():

it("creating a board", () => {
  cy.intercept("/api/boards").as("matchedUrl");
  cy.visit("/");

  cy.get('[data-cy="create-board"]').click();

  cy.get("[data-cy=new-board-input]").type("new board{enter}");
});

You can visit the official Cypress documentation to find out more about the cy.intercept() command.

Allow Cypress to handle animations

As previously mentioned, animations will execute asynchronously, and there can be cases where you’ll want to wait for an animation to complete before proceeding to the next action in the test. Fortunately, Cypress automatically detects if an element is animating and then waits until that element stops animating. Cypress does this by taking a sample of an element’s position over time and then determining if the element has moved recently.

Conclusion

In this article, we have taken a look at synchronous and asynchronous programming, components in the browser that load sync and async, Cypress, and the relationship between Cypress and asynchronously. We looked at how Cypress handles promises internally, enabling us to write much cleaner tests that are easily understood and readable.

We also looked at strategies that can be used to handle asynchronous code in Cypress, including avoiding the use of cy.wait() for halting tests, allowing Cypress to handle animation, and using cy.intercept() for handling network requests.

Try Reflect: A testing platform with built-in handling of async actions

Reflect is a great alternative to Cypress that lets you build and run tests for your web application without writing code. Reflect automatically detects when asynchronous actions occur (including animations, page loads, and slow network requests), and waits for them to complete before proceeding on to the next test step.

Tired of flaky tests? Try Reflect for free.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © 2022 Reflect Software Inc. All Rights Reserved.