How To Avoid Code Smell in Cypress

Improve your waiting game in Cypress testing.

EarthCtzn
5 min readDec 5, 2021
Photo by Julia M Cameron from Pexels

Welcome back everyone and thank you for stopping by again! After a busy/harsh 2021, I am back with a few hopefully helpful bits of knowledge I acquired on the Cypress testing suite. As they describe it. Cypress is a JavaScript End to End testing suite that offers:

Fast, easy and reliable testing for anything that runs in a browser.

I do agree, I have worked with other JS-based testing suites and they are fairly similar. I felt Cypress was easy to learn and start writing and refactoring tests. All right! Let’s get to it 💪.

The Test

This test is navigating our UI to create an Assembly Project. It is navigating to the form, filling out the form, submitting the form, and lastly checking that the project was successfully created.

The Smell

You may be wondering why I am referring to the cy.wait() command as being “smelly”. Don’t get me wrong, it’s not the method but the way we implement it that makes the smell.

To give you some context, cy.wait()allows you to wait for something to finish before doing something else. You can pass it a Time argument (number in milliseconds ) and Cypress will wait that amount of time before continuing with the next step in your test.

What makes it a smell is setting an arbitrary amount of time to wait for a page to load when you are waiting for an XHR request to resolve for the page to load. I was recently tasked with refactoring our test suite as we initially used time in all cy.wait() calls.

In our example test, we are using an NPM package called cypress-wait-until. This package gives us the method cy.waitUntil() which helps our test pass because it sets a timeout (again hard time-based pause) This is great in other cases but here it is not doing what needs to happen which is waiting for an XHR request to finish. We are also adding an extra dependency. This is not necessarily bad but having few dependencies is a very nice thing.

Notice the use of cy.wait(1000)

Above, we used the .waitUntil() method but ended up adding an extra 1-second wait in case the server took longer to respond.

The Problem

The behavior we see in the User Interface or UI for short, as a user is that we click some button and we have to wait for the result to load on the screen. Behind the scenes, we are triggering a GET request to some endpoint controlled by a separate API and waiting for the response to populate the screen. This can happen in an instant or for unexpected reasons, can take a few seconds or even longer.

Adding arbitrary amounts of time to wait for the app to respond will end up adding a bunch of extra time to the total execution time of the test suite and it likely won’t be a very accurate representation of what a user does when interacting with your application. We need to make it faster and more accurate.

Most tech companies have some sort of CI/CD pipeline (Continuous Integration / Continuous Delivery). To put it simply, this means that another system is going to run all tests before any new code is deployed. If these tests don’t pass the code is not deployed and the Dev that wrote it will have to address the test failure.

Systems like Travis CI will spin up virtual machines to deploy and test your code. Herein lies our trouble. When we run our tests locally with the hard waits they may be passing but it’s a different story when automation is doing this in your AWS or Heroku suite.

The Solution

As shown in line 26 above, we click the “Search Items” button and need to wait for the GET request to finish before attempting the next steps.

As mentioned, the cy.wait()method can take a number as an argument but it can also take a string. The more “Best Practice” solution I found is to use Cypress’ .intercept() method to “intercept” our XHR request and chain the .as() method to create an Alias (string) argument to pass-in to cy.wait().

Output from cypress app console.

On line 39 above, we see Cypress getting the search form input field. On line 42, we type in our item name, and on line 44 we click the “Search Item” button to submit the search form. Notice that sneaky XHR between lines 44 and 45 sending out a GET request to the /search/items endpoint.

Now that we know the endpoint we can write our intercept statement as follows.

cy.intercept(“GET”, “/search/items*”).as(“getPants”);
NOTE: We want to let the test know it has to do this before anything else so we add this below the it(my-test-name) block. as in line 11 above.

Let’s break this down. Here we are passing 2 arguments to the .intercept() method. The request type, in this case, is a GET request and the actual route. Notice the*in the route. This is a wildcard that says include everything after this point. So, even if our route is super long with search parameters we will still catch it.

In the .as() method, since we are searching for an item with an SKU of FANCY-PANTS-0 I opted for naming the Alias "getPants". Then when we want to wait for this intercepted route, we use the alias as follows.

cy.wait('@getPants')

Notice the @ symbol preceding the alias. This is a requirement of Cypress. This call will simply wait until the GET request with the alias "@getPants" finishes before moving on to the next step. Now in our test, we can replace the cy.waitUntil() call on lines 33 to 41 below with cy.wait('@getPants') line 32 below.

When we run the test we know we intercepted the correct request because we can see our alias.

And that’s it! I hope this was helpful, thanks again for stopping by! Let me know if you have run into this in your projects and if you have found different ways to get around this. Stay healthy, stay curious!

--

--

EarthCtzn

Full-Stack web developer having fun with Rails, JavaScript, HTML, CSS, React, Redux, Bootstrap, and making things.