What I’ve Learned Using Cypress.io for the Past Three Weeks

Cameron Choi
Level Up Coding
Published in
8 min readJan 18, 2019

--

If you’re reading this blog, you have likely heard of end-to-end testing, integration testing, unit testing, and UI testing. However, just three weeks ago, these phrases were completely unknown to me. In fact, software testing as a whole was mostly foreign concept to me.

Only three weeks ago, I was in the middle of my coding bootcamp finishing up with the first of two projects that were required to graduate. For my second project, I was privileged with an opportunity to start an internship at a software testing startup called HBSmith. As a part of this internship, along with a partner of mine, I was tasked with a project of using Cypress.io to create UI tests for well-known sites. This would be used as promotional material on the homepage of the official company website.

As a result, for the past few weeks I have devoted my life to learning the intricacies of Cypress.io, software testing, and the applications it can have. In this blog, I hope to organize my thoughts and share my newly gained knowledge.

What is Cypress?

Cypress is a Javascript End-to-End testing framework which allows you to efficiently and logically write tests for front-end code and UI. In the past this has been known to be notoriously difficult. It allows tests that can simulate normal user interaction in a web application, while also providing a host of extra information and tools.

Cypress is a relatively new testing framework, so there are still some bugs and missing features. However, it is generally well-regarded and is being actively developed. In the past, Selenium was primarily the sole method to build end-to-end tests, but in recent years there has been an upsurge of these testing tools with Cypress being one of the forerunners.

So why Cypress?

Selenium is almost a 15-year-old technology. During that time, the web has changed drastically and thus, testing should too. Cypress is a completely new testing framework that has been tailor-made for the highly responsive, asynchronous nature of the web today. This allows for fast and consistent test results. Below are some of its key features and advantages that have been advertised on its official website.

Debuggability

  • You are able to debug directly using familiar tools such as Chrome Dev Tools. The hundreds of custom error messages and stack traces also make it easy to discover where the test failed.

Automatic Waiting

  • Cypress automatically waits for commands and assertions before moving on. No more async hell. Cypress will wait for elements to exist, wait for elements to stop animating, or wait for specific network requests to finish.

Practical Architecture

  • Most testing tools (like Selenium) operate by running outside of the browser and executing remote commands across the network. Cypress instead executes in the same loop as your application. This allows native access to every single object — the window, the document, a DOM element, the application instance, etc.

Easy Shortcuts

  • Cypress prevents you from being forced to always act like a user to generate the state for a given situation. Instead, you can programmatically interact and control applications. (This will be shown later)

Cypress in Action

In this section, I hope to display some of Cypress’ key features and commands. I will also include example code from the official docs and my own projects.

cy.visit

This command will visit a specified URL, which then allows you to interact with elements on the page.

cy.visit(‘https://slack.com/signin')

The best practice for this command is to set a baseUrl in the cypress.json file, which will then allow you to omit passing it into this command. By setting a baseUrl, you can also avoid wasting precious time when the tests are first run, as Cypress will load the main window straight into the specified baseUrl.

In cypress.json

{
"baseUrl": "https://slack.com"
}

In exampletest.js

cy.visit(‘/signin’)

cy.get / cy.contains

These two commands are the backbone of any UI test made in Cypress. They serve two main purposes.

  1. To check if a certain element or text is being correctly displayed
  2. To chain an action or assertion off the specified element

Ideally, elements should be selected based on data-* attributes as classes and ID’s can dynamically change. However, if this is not possible, just make sure that the selector you choose to use (class, id, etc.) will not be susceptible to change.

For example, for a button that looks like this:

<button id="main" class="btn btn-large" data-cy="submit">
Submit
</button>

The best practice would be to use:

cy.get(‘[data-cy=submit]’).click()// ORcy.contains(‘Submit’).click()

These commands are similar. The get method allows you to find nodes based on its elements such as class or id and the contains method finds a node based on the test inside of it.

Cypress has also added a feature in its test browser called ‘Selector Playground’ that allows you to quickly find the selector for any element on the screen, as shown below.

Always make sure to double check the selector as I found it was not 100% reliable.

cy.request

As stated before, Cypress allows easy shortcuts through certain commands that prevent you from always having to act like a user. cy.request is one of these commands and it allows you to make HTTP requests that will actually appear to have actually come from the browser. This command can most notably be used to generate the state of a logged in user. It can automatically get and set cookies allowing you completely bypass wasting time always using the UI to login.

cy.request({
method: 'POST',
url: '/login',
form: true,
body: {
username: 'hbsmith',
password: 'password123'
}
})
cy.getCookie('cypress-session-cookie').should('exist')

In the example above, a POST request is sent to the ‘/login’ endpoint, and through this, the cypress-session-cookie will automatically be set in the browser, allowing the state of being logged in to be achieved.

A good practice would be to wrap this cy.request in a custom Cypress command and place it in the cypress/support/commands.js file so that it can be reused throughout the whole test folder structure. As shown below:

In commands.js

Cypress.Commands.add('login', function () {
cy.request({
method: 'POST',
url: '/login',
form: true,
body: {
username: 'hbsmith',
password: 'password123'
}
})
})
// Thus, cy.login can be used in all other files

cy.wait

This command allows you to wait for a specified amount of time, or wait for an aliased request to resolve before moving onto the next command. This command is especially useful for when there is client-side rendering involved that Cypress cannot fully recognise.

Although you are able to wait for a resource to load based on an arbitrarily set amount of time, as shown below:

cy.wait(1000) // wait 1 second

This is not recommended as there are better methods to achieve the same result. One such method is to instead wait for an aliased XHR to respond. The following example shows how to wait on a request.

Trello: Create a list and drag and drop cards to newly created list.

cy.server()
cy.route(‘POST’, ‘/1/lists').as('postList')
createCards()createList() // sends POST request to ‘/1/lists'cy.wait(‘@postList’)dragAndDropCardsToList()

In the example above, an alias is set for POST requests to the ‘/1/lists’ endpoint through cy.route and cy.server. Then, when a list is created and needs time to load, a cy.wait command is called to the aliased ‘postList’ route.

This achieves two main things.

  1. Cypress knows to wait for the new list to be fully loaded and created before the cards are dragged and dropped.
  2. Decreases test flakiness by making sure the proper request is being sent out when a new list is created, and a proper response is coming in return.

Overcoming Cypress’ Limitations

As of now Cypress does not natively support iframes and testing file uploads. In this section, however, I will introduce temporary workarounds that I found overcome both of these issues.

iframe

Due to an oversight in the driver, Cypress does not support targeting or interacting with any elements in an iframe. When working on implementing tests for the collaboration tool, Quip, I came across this issue, but luckily I found an easy enough solution.

First, if you are dealing with cross-origin iframes, chromeWebSecurity will need to be set to false in cypress.json.

{
"chromeWebSecurity": false
}

Then, in order to select elements within the iframe, you will have to:

  1. cy.get the iframe you want to target
  2. cy.find the element within the iframes contents
  3. cy.wrap the element in order to chain proper Cypress commands off it
cy.get('iframe').then($iframe => {
const $body = $iframe.contents().find('body')
cy.wrap($body)
.find('button')
.click()
})

I found the most efficient way of achieving this, was to put this all in a custom Cypress command.

Cypress.Commands.add('iframe', (iframeSelector, elSelector) => {
return cy
.get(`iframe${iframeSelector || ''}`, { timeout: 10000 })
.should($iframe => {
expect($iframe.contents().find(elSelector||'body')).to.exist
})
.then($iframe => {
return cy.wrap($iframe.contents().find('body'))
})
})

Thus, you would be able to alias certain iframes and through the alias, efficiently chain Cypress commands.

Quip: Create calendar events

cy.iframe('[title="Calendar"]').as('calenderIframe') // create aliascy.get('@calenderIframe') // use alias to efficiently chain commands
.find('.Calendar__day')
.eq(0)
.click()
cy.get('@calenderIframe')
.find('.Calendar__day')
.eq(1)
.click()
cy.get('@calenderIframe')
.find('.Calendar__day')
.eq(2)
.click()

File Uploads

I came across this issue while I was implementing a test for Slack’s file upload capabilities. As of now, Cypress does not yet have native event support so there is not an official method for testing this specific function. However, as a temporary workaround where you are able to utilise the Blob library that has been baked into Cypress. How you would do this would differ slightly based on how the upload code is written for the application, I found the basic test structure for uploading an image file to look like the following.

Slack: Upload image to input element

cy.fixture('images/image.png').as('image')

cy.get('input[type=file]').then($input => {
return Cypress.Blob.base64StringToBlob(this.image,
'image/png').then(
blob => {
const imageFile = new File([blob], 'image.png', { type:
'image/png' })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(imageFile)
$input[0].files = dataTransfer.files
}
)
})

First place the image you would like to upload into the fixture folder, and then you would load the image data with cy.fixture. Then, you would cy.get the input element that you would like to upload to, and chain a then command. Using the alias you set to the image fixture, and then convert the image data into a Blob. In this Blob form, you are finally able to transfer it into the input element.

Conclusion

Although Cypress.io is still in its infancy, the sheer amount of features and capabilities make it already a top framework. Especially in this day and age where testing and CI/CD is a crucial part of any software development process, a fast and reliable testing framework is essential. Cypress may not completely fill all the requirements as of now, but I have no doubt it will continue to develop and eventually overcome its limitations.

--

--