Optimizing tests in Cypress

Lleïr García Boada, Quality Engineer at SNGULAR

Lleïr García Boada

Quality Engineer at SNGULAR

July 16, 2021

In the world of quality, automated tests are all the rage. They’re present across the development cycle and are a crucial element to understanding our project’s health.

One of the virtues that our automated tests should have is execution speed. They should be run in the least time possible so you can get your results ASAP. This helps us analyze and make faster decisions.

The proper management of test data is also key.It isn’t simple. You or somebody on your team has to create data and save it in a database and in many cases that data could be changed or even eliminated. This can cause problems like slowing down subsequent tests, not being able to find data or delaying results and, therefore, slowing down decision making.

That’s why we’re looking at how to increase the speed of our tests, how to create shortcuts, simplify data management and use mock data.

Mock data is data that doesn’t come from a backend or service, or even if it does, is considered “false” data since we write it at will. They are not real data that come from the application itself, and are instead used for our developments or test automation.

What will we learn?

Cypress! Cypress is a magnificent javascript framework to quickly and easily build and execute automatic E2E tests on web applications.

But today, in this article, we will primarily focus on three things:

  • Mock data
  • Modification of the LocalStorage and browser cookies.
  • API rest requests.

The main goal we’re after with these techniques is optimizing the execution of tests since they allow us to establish the right context for each test scenario without having to perform actions on the UI.

We’ll go over each functionality in action, applying it to a real-world example.

What makes it interesting?

Preparing data for tests is not only interesting, but it can be critical for:

  • Saving time in defining tests: One of the classic problems in the world of tests is data... “Ah! We don't have this type of user, we have to create one" or "We have to ask the backend team to prepare this data for us to test "... these common statements end with Cypress. From this tool, you can mock up any request, set values in the localStorage/browser session or even make a rest API request (as long as the endpoint exists!). This makes it easier to define a test without having to think about what data you’ll need, giving us greater versatility when it comes to the testing strategy.

  • Saving time in test execution: There are many ways to prepare data. At times, we’ve even had to prepare the data in the test execution itself, on the website (going here, inserting some data, clicking there, updating that…), or sometimes even calling the database and updating. Then, we have to clean all the data for the next executions. The run time goes way down with Cypress since you can mock data really quickly.

  • We manage all the data in Cypress. I already mentioned this in the first point, but I want to make it super clear with its own section. We don’t depend on anyone when it comes to data. Everything is "mockable," even external services.

Where and when do you use it?

We are going to apply these data mocking techniques in our test code and whenever we need to have data in advance. One option is to mock network requests that come to us from the server. This technique is highly useful when we have a service up and running but not all the data we need in the database. This is when we capture the call and mock the data based on our needs.

Intercept requests

For example: we need a test to validate that a list is empty and contains a certain message. What usually happens is that within our application, the list is empty or completely full. It usually starts to fill up when the “add items” functionality is available. External services may even return lists that are always full of items. This complicates data management because we depend on the external service or even cleaning the list through the database with a restore or using another user for the test.

With Cypress, it’s as easy as intercepting the call that the list returns and specifying the items that you need. In this case, zero items. From there, we use the data that we want in the test.

Setting up the state of the browser

Another example: we’re working with an e-commerce business and want to start the test with our local currency, in this case, the euro. But, when we open the page we get US dollars by default. Normally, this is located in the localStorage of the browser and instead of changing the currency with actions in the browser (clicking on the currencies drop-down menu, selecting euro, and waiting for the page to load again), we can edit the variable in the localStorage so the euro currency will show up first.

REST requests from the test code

A final example for the API rest request:

Here, we might have to log in and get a session token for our page so we can get access directly without having to login through the UI itself.

Let's do some practical examples

Now, we’ll look at the code in action, using different pages as examples so it can be visualized.

Mock data

For this example, we’ll use a functional case that could be real. In this scenario, we want to check to see if there’s an alert message when there’s just one item in the following list:

1

Normally, a list starts getting filled up with items that are either introduced by the app or through the database. Later on, it makes it hard to do tests with fewer or even no items.

These values come from a request made to the server. Cypress allows us to intercept the call and tell it what values it will have. We can either have it specified in the code itself or have a JSON as a fixture and use it as a mock. We’re using this page: https://rahulshettyacademy.com/angularAppdemo/ for the next example.

cy.intercept({
    method: "GET",           <i><span style="font-weight: 400;">// type of response that will be intercepted</span></i>
    url: "/Library/**"       <i><span style="font-weight: 400;">//</span></i><i><span style="font-weight: 400;"> URL to search to intercept</span></i><i><span style="font-weight: 400;">//</span></i> (you can use special characters)   
},
{
    statusCode: 200,         // <i><span style="font-weight: 400;">we mock the status code and the response in the body</span></i>
    body : [
       {
          "book_name":"RestAssured with Java",
          "isbn":"RSU",
          "aisle":"2301"
       }
    ]
}).as("books");             // <i><span style="font-weight: 400;">we specify an alias to know which interceptor it is</span></i>

cy.get("[data-target='#exampleModal']").click();    // <i><span style="font-weight: 400;">we navigate or act through the</span></i>          
                                                    // app, <i><span style="font-weight: 400;">where the endpoint is called so</span></i>       
                                                    // <i><span style="font-weight: 400;">that Cypress intercepts it.</span></i> 

cy.wait("@books");         // <i><span style="font-weight: 400;">We use the Cypress wait until we find</span></i> 
                           // <i><span style="font-weight: 400;">the request to intercept with the alias "books"</span></i>
                           // <i><span style="font-weight: 400;">finally, we can do the corresponding validation</span></i>

After the execution, the result will be the following:

2

3

As you can see, the response returns one item and the page shows us the alert message (underlined in yellow) that we should validate for this example.

In the Cyprus Test Runner, we can easily see what we’ve intercepted in the “routes” section.

4

We can make it even more simple and specify just the URL to intercept and the JSON that has the data to mock. That produces cleaner code:

cy.intercept('GET', '/Library/*', { fixture: books.json' })

There are more attributes to mock if we want to:

headers?: { [key: string]: string }

Headers that accompany the response

forceNetworkError?: boolean

If this option is true, Cypress destroys the connection with the browser and doesn’t send a response. That's useful to simulate connection errors with the server.

throttleKbps?: number

Kilobytes per second when sending the body.

delay?: number

Milliseconds to trigger a delay before the response is sent.

Modifying localStorage and cookies

The Storage object (web storage API) allows us to store data locally in the browser without needing to connect to a database. Here, you can find properties like the session token for a user who logged in, the currency of a marketplace or even the language that is on the website right now, among many other things.

In this example, we can see how to change a property that locates and shows the website’s language. We’ll base it on the page: https://www.pullandbear.com/.

Why do you have to specify values for properties in localStorage?

Basically, to prepare tests. This way, we won’t have to specify for each test or group of tests and make it functional from our automated test. As a consequence, the execution time is reduced, as is the possibility for negative errors that can be caused by going through places of our application that aren’t necessary at the moment.

So, to modify an attribute, we’ll use localStorage.setItem(property, value)

cy.visit("https://www.pullandbear.com/")<br></br>localStorage.setItem("LAST_STORE_GEOBLOCKING", '{"storeId":25009475,"langId":-1}')

You always have to specify these values after you’ve gone to the page in question. If not, once you go there, it will be overwritten.

And as you can see, among other properties, we have the one that we’ve just specified:

5

Now, to validate that it was correctly applied, we can see that the URL contains the country (http://www.pullandbear.com/ba/). Also, within the app, there are sections where you can validate that, in this case, Bosnia and Herzegovina as well as the language.

6

In the cookie, we’ll also find values that we can modify. This example of the Pull & Bear page will serve to accept the cookies policy so that we don’t have to accept it each time we start a new test or group of tests. Just like what happens with localStorage, it speeds up the test execution.

Being a saved property in the cookies themselves, it’s as easy as specifying the cookie and its value:

cy.setCookie("OptanonAlertBoxClosed", "2021-06-15T13:42:44.998Z")

The cookie should be specified before we open the page where it will take effect. Then, you’ll see it in the runner as “setCookie.”

7

Requests to an API rest

For this example, we’ll use the page: https://www.checkli.com/ (it’s a simple page with a task list) where we’ll need to have defined the tasks in advance.

The difference between using mock data and making a request and introducing the data is basically because we’ll need this data in other tests and we don’t want to mock them each time.

To see it more clearly, we can use a mock so that an external service where we can’t easily introduce data returns what we need. But, if we have already developed the endpoints, we can use an API rest request that will allow us to introduce values, and nurture, for example, a list.

With that, our list of “TO-DOs” is empty:

8

And we’ll fill it out with 1 task:

cy.request({
     method: "POST",          // d<i><span style="font-weight: 400;">efine the type of request </span></i>
     url: "https://www.checkli.com/api/v1/checklists/"+url+"/tasks", 
                              //<i><span style="font-weight: 400;"> define the request url (in this example we</span></i>                                  
     body:                    <i><span style="font-weight: 400;">// use a string from the url itself)</span></i>
     {
        "name": "Tarea de prueba 1”,       <i><span style="font-weight: 400;">// we define the values to send from the body</span></i>
        "section": 0,
        "priority": 0
     }
});
cy.reload();                <i><span style="font-weight: 400;">// reload the page to see the list filled up</span></i>.

Executing this code, we’ll see that we’ll already have the tasks on the page:

9

Here you can see the complete and functional code with two tasks, one of which is complete:

visit("https://www.checkli.com/")
get("a").contains("Make a free checklist").click();
cy.wait(4000)

cy.url().then(url => {         <i><span style="font-weight: 400;">// in this specific cases, we need the url value</span></i>
    url = url.substr(url.lastIndexOf('/')+1, url.length-1);

    let first = true;
    const tasks = ["prueba 1", "prueba 2"];     // 2 hardcoded tasks
    tasks.forEach(t => {
        cy.request({
            method: "POST",                    
            url: "https://www.checkli.com/api/v1/checklists/"+url+"/tasks",               
            body :                             
            {
               "name": t,
               "section": 0,
               "priority": 0
            }
         }).then(resp => {         <i><span style="font-weight: 400;">// we chain the "request" with the"then" command to</span></i><i><span style="font-weight: 400;">// use variables from the “response</span></i> 
           if(first){              <i><span style="font-weight: 400;">// boolean decision to complete the first task </span></i>
               cy.request({        <i><span style="font-weight: 400;">// make another request based on the task ID</span></i>
                   method: "POST", <i><span style="font-weight: 400;">//</span></i><i><span style="font-weight: 400;"> that we got back the last time</span></i>
                   url: "https://www.checkli.com/api/v1/checklists/"+url+"/tasks/" + resp.body.data.id,

                   body :
                   {
                       "section": 0,
                       "name": resp.body.data.name,
                       "complete": 1
                   }
               })
           }
           first = false;
       })
    })
})
cy.reload()

As we saw in the mock data example, there are tons of attributes to specify in the request that can be found in the Cypress Documentation.

*(This example uses an existing ID in the URL. That’s why it is passed as a value to the request’s URL attribute. Also, when we finish the request, we reload the page since we are on the same page of the list. If we don’t reload, we won’t see the new tasks. Normally, these types of requests are made with static URLs, without passing any type of attribute (although sometimes it is necessary). It allows us to define all the test data in a "before" method, before executing the test or the group of tests to prepare, so that once the test in the application arrives at the point where it will need that data, it will be available)

Conclusions

As we’ve seen, Cypress is a very powerful tool that facilitates data mocking and pre-test preparation. It helps us a lot when it comes to preparing test scenarios quickly and simply, from the direct code of the tests and without having to depend on or wait for other people or get too complicated. At the same time, this all has a positive impact on the definition and execution of tests.

References

https://www.cypress.io/

https://docs.cypress.io/guides/guides/network-requests

https://docs.cypress.io/api/commands/setcookie

https://developer.mozilla.org/es/docs/Web/API/Storage/setItem

Lleïr García Boada, Quality Engineer at SNGULAR

Lleïr García Boada

Quality Engineer at SNGULAR