Automate your Web Application testing with Cypress


Automating the test cycle is an important step in your journey to DevOps. We have seen several test automation frameworks evolve over the years. Ranging from the primitive record and play tools, we have seen the evolution of data driven, keyword driven, Hybrid and now also AI driven test automation frameworks. These test frameworks have played an important role in the evolution from the old Waterfall lifecycle to a new Agile DevOps model that enables delivery per minute.

I have a long experience in testing web applications. I have seen different scales of projects from small company websites to enterprise portals. Over the past 13 years, I have worked on various testing platforms like Selenium, Cucumber, TestComplete and also the traditional manual testing.

Last year, I was introduced to Cypress.io. I found it really interesting. It combines ease of use with a wide landscape of functionality. It is a good tool for automating UI testing. It provides a very good support for responsive as well as single page web pages. It gels very well with the React, Angular and other JavaScript Frameworks.

Writing this blog to share my experience during these days.

Cypress.io


Cypress is a JavaScript based End to End Web Testing framework. Unlike most of the web testing frameworks, it does not carry the burden of Selenium. Cypress has a new architecture from the ground up. Selenium is based on executing remote commands through the network. But, Cypress runs in the same run-loop as your application. This makes it a lot more efficient, as the Cypress test cases run right in the browser, rather than through a driver.

Cypress can be used to test anything that runs in a web browser. It fits the modern JavaScript frameworks like React, Angular, Vue, Elm, etc. very well. Cypress is pure JavaScript, and does not depend on any external drivers. Hence it can be easily integrated with the development - enabling a meaningful test driven development.

Unlike most other frameworks Cypress is designed for end to end testing of web applications rather than testing individual use cases. It is designed to be easy to work with. It is ideal for the developers as well as for the QA engineers.

Let us now look into a small example of using the Cypress framework.

Installation


Cypress is based on JavaScript. Best used with a NodeJS. We can install it on the command line using the npm.

If you have a recent NodeJS installation, it is a simple task to install the Cypress module

$ npm install cypress --save-dev

> cypress@4.0.1 postinstall /home/vina/dev/cypress/node_modules/cypress
> node index.js --exec install

Installing Cypress (version: 4.0.1)

  ✔  Downloaded Cypress
  ✔  Unzipped Cypress
  ✔  Finished Installation /home/vina/.cache/Cypress/4.0.1

You can now open Cypress by running: node_modules/.bin/cypress open

https://on.cypress.io/installing-cypress

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN learn@1.0.0 No description
npm WARN learn@1.0.0 No repository field.

+ cypress@4.0.1
added 214 packages from 137 contributors and audited 278 packages in 195.72s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

$

If you are working on Windows, you will need additional modules

npm install -g win-node-env
npm install --global --production windows-build-tools

NPM install usually does not have any problems. If any, you can check out the Cypress support page. It has a good documentation to help you begin with. If all is well, we can proceed with the code.

Using Cypress


Once Cypress is installed, we can open it using the below command

$ ./node_modules/.bin/cypress open
It looks like this is your first time using Cypress: 4.0.1

  ✔  Verified Cypress! /home/vina/.cache/Cypress/4.0.1/Cypress

Opening Cypress...

Or we can add the Cypress command to the package.json so that we can open it directly from the prompt.

Add to the scripts section in the package.json

{
  "scripts": {
    "cypress:open": "cypress open"
  }
}

With this, you can open cypress using the command

$ npm run cypress:open

> learn@1.0.0 cypress:open /home/vina/dev/cypress
> cypress open

This opens an application window that looks like this:

loading...

 

Accept the prompt and you will be led to the integration test development environment. You can see on the top right corner - Cypress allows us to specifically choose a browser for working. This list will vary depending upon the installation.

loading...

Try clicking one of these *.spec.js files to get a glimpse of what Cypress can do. Liked it? Then let's go ahead and try to implement a test specification ourselves.

Dummy Test Cases


In your project folder, type this to create a new spec file

$ touch cypress/integration/test_spec.js

As soon as we do this, we can see it in the Cypress test runner window. We just created an empty test spec. But, to have some fun, just try to run that spec by clicking on the Cypress code runner tool. It opens up the browser, and then shows an error that there is no test defined in the test spec.

That was expected!

Let's now try to write a dummy test case to understand the concept. Add this to the test_spec.js

describe('Dummy Test', function() {
  it('Always Pass!', function() {
    expect(true).to.equal(true)
  })
})

This is the generic syntax of a Cypress test case. The elements of the syntax are self explanatory.

We can also use the modern JavaScript syntax:

describe('Dummy Test', () => {
  it('Always Pass!', () => {
    expect(true).to.equal(true)
  })
})

The result is identical. Now, let's work on a more meaningful test case.

Sample Test Case


We all know about the three aspects of a test case - “Given, When, Then”, or “Arrange, Act, Assert”. The idea is that we put the application into a specific state, then take a specific action in the application that causes it to change, and finally we check the resulting application state, to assert if it is what we expect it to be.

Similarly, we can create a test case in Cypress. To demonstrate an example, let us view the below script:

describe('Mirum India', () => {
  it('Check our Website', () => {
    cy.visit("https://www.mirumagency.com/en/home")
    cy.contains("View all news").click()
    cy.url().should("include", "/news")
  })
})

Here, we instruct Cypress to visit the given URL. On that page, we check if we can see the text "View all news". If it is there, we click on it. On clicking that link, we are forwarded to another page. On that page, we assert that the URL string includes "/news".

It is simple as that! Anyone with very little programming experience can easily pick this up and begin developing the test cases.

Best Practices for a Real Project


Simple examples are good for learning. But, when we work on a real project, we need a lot more than just syntax. We need to understand the best practices developed over the days as the framework has evolved. Here are the important ones. Check out the Cypress documentation for more details

Folder Structure


The folder structure in my project looked like this.

/cypress
  /fixtures
    - example.json
  /integration
    - actions.spec.js
    - aliasing.spec.js
    ....
  /plugins
    - index.js
    ....
  /selectors
    - userinfo.js
    ....
  /support
    - commands.js
    - index.js
  /tests
    - usersignup.js
    ....
  /utils
    - validation.js
    ....

The cypress folder in the main project folder contains the code related to the cypress testing. Fixtures, Integration, Plugins and Support are generated by Cypress window runner when we initiate the project. We added the others to ensure modularity.

The tests folder contains the real JavaScript code for the test cases. This includes several scripts - each related to an end to end test scenario.

The Selectors are used to select individual elements on the page. Thus, in the above example, instead of selecting the element using the hard coded string "View all news", we could have used a selector defined in this folder. This can help ensure modularity.

The utils folder contains generic utility methods that are required all over the code.

Configuration


Cypress provides for an elaborate configuration of the test environment. For example, we can define the base url and other environment details. A sample minimal cypress.json would look like this.

{
  "baseUrl": "http://localhost:7081",
  "fixturesFolder": false,
  "pluginsFile": false,
  "supportFile": false
}

The cypress.json provides a lot more functionality and can be very powerful if used fully.

Useful Constructs


While working on the project, we found a few useful constructs that we used a lot more than others. Enumerating some of them, for quick reference.

Viewport


Responsive web design is a default requirement for any new web application. Cypress allows us to specify the viewport, to check how the page would look for the given screen size.

cy.viewport(1400,1080)

Invoking this command before any other will ensure the tests are run for the given screen size.

Request


REST API's are an essential part of any single page application. Cypress allows us to invoke the API's directly as a part of the test cases.

const add = (item) => {
    return cy.request('POST', '/todos', item)
}

DOM Query


The text query introduced by Cypress is elegant. But often, we do need DOM queries to make things simpler. Cypress provides elaborate queries to check for DOM elements.

cy.contains('.filters a', 'Active').should('have.class', 'selected')

This ensures that there is an Active a element with class filter, and it should be selected.

Nested Assertions


We can have several nested assertions in a single command. That reduces the complexity of the test cases. For example:

cy.get("@reports")
    .find("a[class*='button__ButtonBase']")
    .should("be.visible")
    .should("have.prop", "href")
    .and("have.css", "text-align", "center")

Iterations


Cypress also supports have iterations over collections, by defining variables for acting on a given element

it("Display Selection", () => {
    cy.get("section[class*=news-media']")
      .as("newsSection")
      .should("be.visible")
      .find("h2")
      .contains("Investor Relations News")
    cy.get("@newsSection")
      .find("article[class*='news-item__NewsItemContainer']")
      .each($article => {
        cy.get($article)
          .find("a")
          .should("have.prop", "href")
          .and("match", /news\/releases/)
        cy.get($article)
          .find("span[class*='news-item__NewsItemLabel']")
          .contains("Press Release")
      })