How to optimize Cypress tests using JavaScript abilities? (Part 2. Recursion)

sanzhanov

Alex Sanzhanov

Posted on April 20, 2023

How to optimize Cypress tests using JavaScript abilities? (Part 2. Recursion)

Greetings to all Cypress enthusiasts!

In the last article, we looked at various cases of optimizing Cypress tests based on the use of loops. I would like to delve a bit into the problem of optimizing repetitive tasks and consider another programming construct used to repeatedly repeat a particular piece of code or set of instructions. This construction is called a recursion.

What exactly does recursion mean?

Recursion is a programming technique that involves a function calling itself within its own code. When a function is called recursively, it solves a problem by breaking it down into smaller and simpler subproblems until it reaches the base case, which is the smallest subproblem that can be solved without further recursion.

How can recursion be used in Cypress tests?

In Cypress automated tests, recursion can be used to iterate through complex nested structures or perform actions on dynamic elements that may not be present when the test is first executed. By breaking down the problem into smaller subproblems, recursion allows for more efficient and modular code that can handle a variety of scenarios. One of the most common cases of using recursion in Cypress tests is testing of pagination (see below).

Why use recursion in Cypress tests when we can use loops instead?

Using recursion may be more suitable than using loops in Cypress tests when the problem or task requires repeated execution of a block of code on a nested structure, or when the problem can be divided into smaller, similar sub-problems. Recursion is also useful when the exact number of iterations needed is not known in advance, or when the nesting level of the elements being tested is unknown or variable. Additionally, recursion can make the code more readable and easier to maintain in cases where loops would require complex conditional statements or multiple nested loops.

Now that we have an understanding of the advantages of recursion for optimizing testing processes, let’s consider several common cases of its use in Cypress tests.

1. Iterating through nested elements

In Cypress tests, it is common to iterate through nested elements to perform various actions like searching for elements, clicking or validating their properties, etc. Recursion can be used in this scenario when the elements are nested in an unknown or dynamic depth, meaning that the number of nested elements can vary.

Let’s consider an example where we want to validate the text of all nested elements inside a parent element. We can define a recursive function that traverses through all the nested elements, checks if it has any children, and then recursively calls the same function on those children:

Image description

In the above example, the validateNestedElements() function takes a parent element, checks if it has any children, and recursively calls itself on each child element until it reaches the leaf elements without any children. Then expect statement is used to validate the text of each leaf element.

Recursion can also be used to iterate through nested elements in a page and perform actions on them. Therefore, this scenario is very suitable for testing pagination when we are going to test moving to each next/previous page:

Image description

Using the visitTextPageIfPossible() function this code repeatedly clicks on the “next” button, waiting for the page to update, until the button becomes disabled, which indicates that there are no more pages to display.

2. Handling asynchronous operations

Recursion can be useful in handling asynchronous operations in Cypress tests when dealing with situations where we need to retry an action until a certain condition is met. For example, suppose we have a test case where we need to wait for an element to appear on the page, but due to network latency or other factors, the element may not appear immediately. In such cases, we can use recursion to retry the action until the element is found or until a certain timeout is reached:

Image description

In this example, the clickElement() function uses recursion to handle the case where the element to be clicked is not immediately available on the page. The function first attempts to locate the element using cy.get() command, with a timeout of 5 seconds. If the element is found, the function clicks on it and returns. If the element is not found, the function recursively calls itself with the same selector and one less retry remaining. This continues until either the element is successfully clicked or the maximum number of retries is reached, at which point an error is thrown.

Using recursion with retries is better than multiple increases in the timeout because it allows us to wait for the element to become visible without blocking the execution of the test. Increasing the timeout multiple times would add unnecessary delays to the test and could make it more prone to flakiness if the element takes longer to appear than expected. Recursion with retries allows us to wait for the element without adding unnecessary delays, and if the element never appears, the test fails quickly rather than waiting for an excessive amount of time.

3. Testing complex user interactions

Recursion can be used in Cypress tests to test complex user interactions that involve multiple steps and dynamic elements. In such scenarios, using recursion can be more suitable than loops because it allows for a more elegant and efficient way of navigating through dynamic UI elements.

For example, let’s say we have a form with a dynamic number of fields, and we want to test that all required fields are filled before submitting the form. We can use recursion to navigate through each field, check if it’s required, and fill it if it’s empty:

Image description

In this example, the fillForm() function takes an array of FormField() objects and an optional index parameter. It checks if the current field at the given index is required and empty, and fills it if needed using Cypress cy.get() and cy.type() commands. It then calls itself recursively with the next index until all fields are processed. Finally, when all fields are processed, the function submits the form using Cypress submit() command.

4. Building complex test flows

Recursion can be used to build complex test flows that require multiple steps and conditions. For example, if you have a test that requires the user to navigate through a series of pages and perform different actions on each page, you can use recursion to build a flow that navigates through the pages and performs the necessary actions.

Let’s consider an example where we want to test a form that has multiple pages, and we need to fill in various fields on each page before we can submit the form. We can use recursion to navigate through the pages, fill in the fields, and move to the next page until we reach the last page:

Image description

In this code, we define an array of FormPage objects, where each object represents a page in the form. Each page has an array of FormField objects, which represent the fields on that page. The next property is a function that is called when the current page is filled out and submitted, which navigates to the next page.

The fillFormPage() function takes an index of the current page and fills out the fields on that page using cy.get() and cy.type() commands. If the current page has a next function defined, it calls that function to move to the next page. The submitForm() function submits the form by clicking the submit button.

5. Dynamic data handling

Recursion can be used to handle dynamic data that changes over time. For example, if you have a list of elements that is updated dynamically, you can use recursion to continuously check for updates and perform actions on the updated elements.

Suppose we have a web page that displays a list of items, where each item has a unique ID and some data associated with it. The data displayed on the page can change dynamically based on various user interactions or other factors. Our Cypress test needs to verify that all the items are displayed correctly on the page, but we don’t know how many items there are or what their IDs are in advance. In this case, we can use recursion to traverse the list of items on the page and check their data one by one until we have checked them all:

Image description

In this code, the checkAllItems() function calls itself recursively with the next index until all items have been checked. The function extracts the data for each item from the corresponding DOM element and performs some assertions on the data.

The reason why recursion is a good fit for this scenario is that we don’t know how many items there are in advance, so we can’t use a traditional loop with a fixed number of iterations. Additionally, the Cypress commands used in the function are asynchronous and return promises, so we need to use recursion to ensure that each item is checked in the correct order.

6. Implementing retries

In Cypress tests, sometimes there may be flaky tests, where the test may fail intermittently due to network latency, server issues, or other reasons. In such scenarios, it’s helpful to retry the test multiple times to increase the likelihood of it passing.

To implement retries in Cypress tests, we can use recursion. The basic idea is that we will recursively call the test function until it passes or until a maximum number of attempts have been made.

Let’s say we have a test that checks whether a login form is working properly. We want to retry this test up to three times if it fails due to some intermittent issue. Here’s an example of how we can implement this using recursion:

Image description

In this example, we define a testLogin() function that takes two arguments: maxAttempts, which is the maximum number of times the test should be retried, and currentAttempt, which is the current attempt number (defaulted to 1).

Inside the function, we first visit the login page and fill in the form. Then we use cy.url().should() to check whether we have been redirected to the dashboard page. If the assertion passes, we do nothing and the test is considered to have passed. If the assertion fails, we check whether we have exceeded the maximum number of attempts. If we have not exceeded the maximum, we recursively call testLogin() with an incremented attempt number. If we have exceeded the maximum, we use an assertion to fail the test.

Final thoughts

In conclusion, recursion is a powerful tool that can greatly enhance the flexibility and functionality of Cypress tests. As we’ve seen in this article, it can be used in a variety of scenarios, from iterating through nested elements to building complex test flows. However, it’s important to note that while recursion can be a useful approach, it may not always be the most efficient option due to potential performance issues.

It’s crucial to evaluate each use case carefully and consider alternative solutions if necessary. Ultimately, the goal is to create Cypress tests that are reliable, maintainable, and efficient. By understanding the strengths and limitations of recursion in testing, we can make informed decisions and optimize our test suites for success.

The source code of all presented examples can be found in the corresponding repository on GitHub. To continue your journey with me and get even more information about testing with the awesome Cypress tool, I invite you to subscribe to my blog “Testing with Cypress”.

Thank you for your attention! Happy testing!

💖 💪 🙅 🚩
sanzhanov
Alex Sanzhanov

Posted on April 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related