In Cycle 2.22, Cycle Labs released the WebDriver step plugin, which provided new implementations for all web steps with cleaner code that ran more efficiently. The plugin also improved the steps’ internal waiting mechanisms so that their interactions would be inherently more reliable without needing extra CycleScript code. As a result, CycleScript code in our libraries and projects for WebDriver steps can be improved. This article explains how waiting works for WebDriver steps and how to write CycleScript code with better practices.
Every web step performs an interaction on a page rendered in a web browser using Selenium WebDriver. However, to be reliable, each interaction needs to wait for its “target” to be “ready” before firing. For example, if a step intends to click a button, then the button must both be displayed on the page and be enabled before the step can send the click action. Otherwise, the click will not find the button, and Selenium will raise an error. Improper waiting is the bane of black box test automation because it inevitably causes intermittent failures.
The target and the readiness condition for each step is different, but they follow common sense:
- Steps that interact with elements wait for the element to be displayed.
- Steps that interact with alerts wait for the alerts to appear.
- Steps that perform verifications wait for the verification condition to become true.
Almost all WebDriver steps perform some kind of waiting. Only a few steps do not have a target (such as “I refresh web browser”) and therefore do not perform internal waiting.
Furthermore, all automatic waiting happens before the interaction is performed. A step does not add waiting after the interaction is performed to verify that it was successful. Instead, follow-up steps must verify any outcomes. For example, if a test clicks a button to open a popup window, the click step does not wait after firing the click for the popup to appear; a separate step must check the popup. The pattern of waiting automatically before and not after interactions ensures that steps wait only where necessary, which keeps tests more efficient overall.
Every wait must have a timeout: an amount of time that the automation will continuously check for the target to be ready until it stops and yields an error. Timeouts ensure that tests do not get stuck in infinite loops waiting for conditions that will never happen. Web steps can set local timeouts with the suffix “within X seconds” or via a global timeout configured in execution settings that is applied to all steps. Explicit local timeouts override the global timeout.
Avoiding anti-patterns
Many scenarios in the libraries and in customer/partner projects add extra CycleScript code for waiting that is no longer required with the new WebDriver plugin. This code was often written with good intentions to perform interactions safely but now is no longer necessary – and is even detrimental. Extra code makes tests harder to maintain, slower to execute, and more confusing to troubleshoot.
Double waiting
“Double waiting” happens when two steps perform the same waiting condition. For example, consider the following CycleScript code:
Once I see element "id:my-button" in web browser within 10 seconds
Then I click element "id:my-button" in web browser within 10 seconds
The first step explicitly waits for the “my-button” element to appear on the page. However, the second step also automatically performs a waiting check for the same “my-button” element before it fires the click action. Therefore, the first step is unnecessary, and the code could be simplified to one line:
When I click element "id:my-button" in web browser within 10 seconds
Since the click step performs an interaction, it would be more correct to use the “When” keyword instead of “Then.”
There are times when tests must perform verifications. For example, the following code verifies that the text “Cycle Labs” appears somewhere on the page:
Once I see "Cycle Labs" in web browser within 5 seconds
This line actually has two layers of waiting:
- The “Once” keyword, which handles it at the level of the language
- The “within 5 seconds” suffix, which handles it at the level of the step
The “Once” keyword is no longer needed for WebDriver steps at all. It is better to let the steps handle waiting internally. This optimizes the execution of the steps. Therefore, the code should be written like this:
Then I see "Cycle Labs" in web browser within 5 seconds
Using the “Then” keyword reinforces that this step performs a verification. Note that different keywords might be more applicable for other steps used in different contexts.
Almost all WebDriver steps have a timeout suffix. The timeout suffix can become repetitive in long tests. For example, multiple steps together can look like this:
When I type "andy" in element "name:username" in web browser within 10 seconds
And I type "cycle123" in element "name:password" in web browser within 10 seconds
And I click element "id:login-button" in web browser within 10 seconds
Then I see "Welcome" in web browser within 10 seconds
For most test projects, web interactions should share a global default timeout value. Typically, this value is 30 seconds, but it could be more or less depending upon the systems under test. Cycle provides a global timeout for WebDriver steps that is configurable as an execution setting. (Note that this timeout setting is particular for WebDriver steps and is separate from other timeout settings.)
Timeout suffixes should not be used to enforce a global timeout. Instead, they should be used only to override the global timeout when absolutely necessary. Hard-coded timeouts are harder to change when they are sprinkled throughout the code. Therefore, the code above could be simplified like this:
When I type "andy" in element "name:username" in web browser
And I type "cycle123" in element "name:password" in web browser
And I click element "id:login-button" in web browser
Then I see "Welcome" in web browser
Even though these steps do not explicitly state a timeout value, they will still perform automatic waiting using the global timeout value. Local timeouts with the “within” clause should be reserved for times when steps need a different timeout from the global timeout.
Timeout variable configurations
In large test projects, it is common to set up multiple timeout variables for different purposes, such as different types of steps or even different interaction lengths. This kind of configuration enables careful execution tuning. It also enables testers to configure all timeouts in one place. This should not be considered an anti-pattern for large projects with multiple step types where testers are familiar with the pattern.
Hard waits
Hard waits force test execution to pause for a given amount of time before proceeding. For example:
When I wait 10 seconds
And I click element "id:my-button" in web browser
In this code, execution pauses for a full 10 seconds before firing the click action. Contrast it to this code:
When I click element "id:my-button" in web browser within 10 seconds
The automatic waiting in the click step is a “smart” wait in that it will repeatedly check if the button becomes ready during the timeout period. So, if the button becomes ready after only 2 seconds, the step will identify that it is ready, stop waiting, and fire the click. The 10 seconds specified in the suffix represents the maximum amount of time the step will wait, not the minimum amount of time the step must take.
Hard waits are one of the worst anti-patterns in black box test automation because they force tests to take much more time than necessary. They also do not guarantee that the target for the step is actually ready to receive interaction. Instead, a hard wait just presumes that the system will make the target ready within the given waiting time. Therefore, hard waits should be avoided as much as possible.
Applying better practices
Please keep these practices in mind when writing CycleScript code. Note that they are applicable only for WebDriver steps. Other kinds of steps might still need explicit safety checks or the “Once” keyword.