The inexorable(?) ticking TIME bomb!
ACT 1: Exposition
How often have you seen a cy.wait(5000)
in a peer's pull request, or a cy.wait(10000)
in a stubbornly flaky test that someone asked you to review because it's not always passing?
I'd wager 0.0005 gold tokens of membership (approximately $1) that it occurs to you several times in a week!
Why is cy.wait(TIME)
labeled as an anti-pattern in testing, yet it's frequently used by so many QA engineers and testers, even if only occasionally?
There is an easy answer: it is convenient. Very convenient! But at what cost?
ACT 2: Confrontation
In my opinion, the cost can be considerably high:
It introduces unnecessary delays, making your tests slower.
It introduces dangerous uncertainty, which can make your tests unpredictable and, in other words, FLAKY—a term dreaded by testers.
However, there's no need to despair; we have several alternatives to the notorious cy.wait(TIME)
at our disposal. These options can be added to your everyday toolkit and are definitely more deterministic strategies for synchronizing your tests with the application's state.
But what are these alternatives?
Timeout override option
Below is one of the most common "patterns" I have observed in test automation:
cy.get('[data-cy="calculate-totals"]').click();
cy.wait(10000);
cy.get('[data-cy="total-value"]').should('be.equal', '$125');
The cy.wait(10000)
command suggests that the test waits for 10 seconds after clicking the calculate-totals
button to allow time for an operation, such as an asynchronous calculation or data retrieval, to complete before it checks the result. By default, Cypress will fail an assertion if the 4-second default timeout is exceeded.
However, this code will always wait the full 10 seconds before checking the assertion, even if the calculation takes less time.
A more efficient approach would be to use a custom timeout for this specific assertion, accommodating potentially lengthy calculations:
cy.get('[data-cy="calculate-totals"]').click();
cy.get('[data-cy="total-value"]', { timeout: 10000).should('be.equal', '$125');
With this simple change, the test will proceed as soon as the calculation is finished, without unnecessarily waiting the full 10 seconds, or it will fail if the 10-second timeout is reached.
You can override the default
pageLoadTimeout
anddefaultCommandTimeout
for all your tests in thecypress.config.js
file.
Wait for Network Requests
How many times have you come across code like this in tests?
cy.visit('/page-that-loads-data');
cy.wait(5000);
// Rest of the test
// [...]
You probably wouldn't be exaggerating if you said hundreds of times!
In this example, cy.wait(5000)
forces the test to pause for 5 seconds after visiting the page, hoping that this will be enough time for the data of the page to load.
Using fixed wait times like this is not advised as it can result in unreliable tests and longer test times, especially if the actual loading time differs from the wait period.
However, if you take a bit more time to analyze how the application behaves, you might notice, for example, that the page displays data returned from an API call to /api/data
. In such scenarios, we can utilize cy.intercept()
and cy.wait(ALIAS)
to our advantage:
cy.intercept('GET', '/api/data').as('getData');
cy.visit('/page-that-loads-data');
cy.wait('@getData');
// Rest of the test
// [...]
This Cypress code performs the following actions:
It sets up an interceptor to monitor a GET request to the
/api/data
endpoint, and once such a request occurs assigns it the aliasgetData
.It navigates to the
/page-that-loads-data
URL, which should trigger the monitored GET request.It pauses the test execution until the aliased
getData
request is completed.
The use of cy.wait('@getData')
helps create a more reliable test by ensuring that subsequent commands only execute after the necessary data has been retrieved.
Note that cy.wait()
also supports the timeout override option, allowing for custom timeout settings.
Wait for DOM Elements on page load
Static wait times often suggest that the test is not properly synchronized with the application's state.
This is a "pattern" you might often encounter in Cypress tests:
cy.visit('/page-that-loads-data');
cy.wait(5000)
// Rest of the test
// [...]
In this example, the test navigates to a page using cy.visit()
and then pauses for a fixed duration of 5 seconds, assuming this will allow enough time for the page to fully load.
Instead of relying on cy.wait(TIME)
, it's better to use cy.get()
with assertions that automatically retry until the presence of specific DOM elements signals the page has loaded. These assertions can be chained to keep retrying until certain conditions are met.
For instance:
cy.visit('/page-that-loads-data');
// Assert data is visible and has 5 elements
cy.get('[data-cy="data-list"]')
.should('be.visible')
.and('have.length', 5)
// Rest of the test
// [...]
In this case, we expect the page to be fully loaded once the DOM element with the data-cy="data-list"
attribute is visible and contains 5 items. Cypress will continue retrying this assertion until it passes or the default timeout is reached.
For pages that may load slowly, consider using this technique with an increased timeout (remember, the default Cypress timeout is only 4 seconds):
cy.visit('/page-that-loads-data');
// Assert data is visible and has 5 elements
cy.get('[data-cy="data-list"]', { timeout: 10000 })
.should('be.visible')
.and('have.length', 5);
// Rest of the test
// [...]
Wait for Routes
When testing a single-page application (SPA) and waiting for route changes, cy.url()
can be used to wait for the URL to update:
cy.get('[data-cy="goto-dashboard"]').click()
cy.url().should('include', '/dashboard')
Keep in mind that cy.url()
is a Cypress command that retries automatically until the assertion is met or a timeout occurs. Additionally, cy.url()
supports the timeout override option, allowing for custom timeout settings.
Wait for Animations
If animations or transitions affect the test flow, consider disabling them in your test environment or use Cypress commands to wait for the animation to complete.
// Trigger the animation
cy.get('#animated-element').click();
// Wait for the animation to complete by checking for a CSS property that indicates completion
// For example, if the animation ends with the element being fully opaque, you could check the 'opacity' property
cy.get('#animated-element').should('have.css', 'opacity', '1');
This example assumes the end state of the animation can be detected by a change in a CSS property.
Custom Wait Commands
If you have a specific condition to wait for, you can create a custom command that uses recursion and cy.wait()
with a short delay to poll for the condition.
Cypress.Commands.add('waitForCondition', (conditionFn) => {
const pollCondition = () => {
if (conditionFn()) {
return
}
cy.wait(100).then(pollCondition)
}
pollCondition()
})
I must say that this is by far my least favorite alternative!
Anything else?
We discussed a comprehensive list of alternatives to cy.wait(TIME)
for multiple scenarios in Cypress testing. But here are a few more techniques that can be considered:
Alias Waiting: Besides waiting for network requests, you can alias almost any command in Cypress with
.as()
and then wait for it withcy.wait()
.-
Page Event Listeners: Sometimes, applications emit custom events when certain actions are completed. Cypress itself does not directly provide a mechanism to listen for custom events on the page as part of its API. However, you can use standard JavaScript within a Cypress test to listen for custom events emitted by your application.
Let's say your application emits a custom event called
dataLoaded
on thewindow
object when data has finished loading. You can create a Cypress test that waits for this event before proceeding:
cy.visit('/page-that-emits-event');
// Create a promise that resolves when the `dataLoaded` event is fired
const waitForDataLoadedEvent = new Cypress.Promise((resolve, reject) => {
cy.window().then((win) => {
win.addEventListener('dataLoaded', resolve);
});
});
// Use the `cy.then()` command to wait for the promise to resolve
cy.then(() => waitForDataLoadedEvent).then(() => {
// The `dataLoaded` event has been fired, and we can continue with our assertions
cy.get('#data-container').should('contain', 'Data loaded');
});
-
Using Plugins: There are plugins available that extend Cypress's capabilities, such as cypress-wait-until by Stefano Magni, which can be used to wait for a certain condition to be true before proceeding.
Here is an example of how to use
cy.waitUntil()
in a Cypress test:
describe('Example using cypress-wait-until', () => {
it('waits for a specific condition to be true', () => {
cy.visit('/page-with-dynamic-content');
// Use cy.waitUntil to wait for a condition to be true
cy.waitUntil(() =>
// In this case, we're waiting for an element with attribute [data-cy="dynamic-element"] to contain the text 'Loaded'
cy.get('[data-cy="dynamic-element"]').then($el => $el.text() === 'Loaded'),
{
errorMsg: 'The element did not load in time', // Custom error message
timeout: 10000, // Timeout after which the waitUntil will fail
interval: 500 // Time to wait between the retries
}
);
// Continue with other actions or assertions after the condition is met
cy.get([data-cy="dynamic-element"]).should('be.visible');
});
});
Conditional Testing: Sometimes, you may need to perform actions based on the state of the application. Cypress allows you to use
.then()
to add custom logic that can decide what to do next based on the current state of the DOM or any other condition.Cypress Clock and Ticking: For controlling time-based functions like
setTimeout
orsetInterval
, Cypress provides thecy.clock()
andcy.tick()
commands, which can be used to test time-dependent code without real waiting.Stubbing and Mocking: When you don't need to test the actual network requests, you can stub or mock them to instantly respond with predefined data, eliminating the need to wait for real network requests to complete.
ACT3: Resolution
By implementing these strategies in your Cypress tests, you can enhance their robustness and reliability. Instead of depending on arbitrary wait times—which can lead to flaky tests and increased execution time—you align your tests with the actual behavior of the application.
This synchronization ensures that your tests wait for specific events, such as the completion of network requests, animations, or the appearance of elements, before proceeding. As a result, your tests are less prone to failure due to timing issues and provide more accurate results.
By leveraging Cypress's built-in commands for dynamic waiting, such as cy.get()
with custom timeouts and cy.intercept()
for network requests, you create a more efficient and stable testing environment that can adapt to the varying response times of your application.
I believe that these tools can be highly effective at defusing the now not inexorable ticking TIME bomb introduced by the use of cy.wait(TIME
) in your automated tests.
Don't forget to subscribe, leave a comment, or give a thumbs up if you found this post useful.
Happy reading!
Top comments (11)
Not sure which is worse, the test suite taking forever to run or the arbitrary interval that may not be correct. Thanks for sharing.
I would say definitely the the application under test that takes that much time to show results to the user. :)
Good point!
The most comprehensive article on wait management. Great one.
Thank you very much @joydeep100 ! 🙌
Nicely written, lots of information, really useful examples, but an approachable style 💯
Thank you @marktnoonan , really appreciated!
Nice! There is almost always a better option than wait, if you understand exactly what outcome you're really waiting for
Thank you Micah. Glad you found it insightful.
Excellent blog post! Thanks for sharing it.
@walmyrlimaesilv thank you very much! 🤲