End-to-End Testing With Cypress


Intro

End-to-end testing can be intimidating. There are different reasons for this intimidation, but ultimately it’s the initial lack of speed when setting up end-to-end testing on a project. That’s where Cypress comes in handy. With Cypress, you can quickly and easily implement crucial tests that help allow for a higher quality product with better integrity. In this article, we will review the initial setup of Cypress, creating a test with Cypress, and expanding the testing further with some real-life use cases.

Prerequisites:

  • Node.js installed.
  • Some JavaScript knowledge.
  • Some Github knowledge.

Initial setup

Setting up Cypress is fairly straightforward.

npm install cypress --save-dev

Cypress is now installed in your project directory as a dependency. Next, we will open the Cypress testing interface and this will add example tests to your project.

You can run this command to open the Cypress testing interface. This interface will list all the tests available in your project.

$(npm bin)/cypress open

Example Cypress tests in Cypress interface
Example Cypress tests in Cypress interface

In your project root directory, you will see a folder labeled “cypress”, the e2e folder contains all the example test specs.

These example test specs are fantastic to review the different use cases. I’d recommend after installing Cypress, creating a few core tests needed for your application.

Creating a test

In the e2e folder let’s create a folder called core_elements, in this folder, we will write tests to check that our website has the HTML elements we need. Next, let’s create a test called footer.spec.js. We will use this test to check that our website has a footer with the expected elements.

Cypress uses JavaScript for its tests, making it very easy for anyone who knows a little JavaScript. Some things can be comparable to jQuery like how the selectors work, so some of the syntaxes might look familiar if you also know jQuery, if not that’s okay too!

Here is an example test in which we will check if this current website has a footer and as a bonus, we will check for a social media link.

/// <reference types="cypress" />

context('Homepage', () => {
  // Before each test, visit the local homepage
  beforeEach(() => {
    cy.visit('http://localhost:8000')
    // If we had a baseUrl set we could do this instead
    // cy.visit('/')
  })

  // Check that a footer exists
  it('Has a footer', () => {
    cy.get('footer').should('exist')
  })

  // Check that a twitter link exists in the footer
  it('Has a twitter link in the footer', () => {
    cy.get('footer .social--twitter').should('exist')
  })
})

Notice the beforeEach brings our test to the page we want to test, then we are using the get() function to evaluate the document element.

In the Cypress interface, this test can be run and a visual output of the test will be displayed.

Example footer test
Example footer test

This is a very simple example, next we will discuss some more realistic use cases you might be able to apply to your project.

Use cases

Cypress has a wide variety of use cases, on the libraries’ website they have a great list of recipes.

Here are a few great example use cases that can quickly impact your website. Let’s take a look at some example code for each test type.

  • Testing login
  • Linting inline CSS elements
  • Continuous integration

Testing login

In this login example, we make an XHR request. Alternatively, you could fill in the actual form interface with the Cypress type() function.

/// <reference types="cypress" />

describe('Logging in using XHR request', function () {
  const username = 'testUsername'
  const password = 'testPassword'

  it('can bypass the UI and still log in', function () {
    cy.request({
      method: 'POST',
      url: 'http://localhost:8000/login',
      body: {
        username,
        password,
      },
    })

    // Check for session cookie
    cy.getCookie('your-site-session-cookie').should('exist')

    // Visit the page behind the authentication
    cy.visit('/profile')
    cy.contains('h1', username)
  })
})

After you set up a working login test with your website, you may want a custom login function that you would call before any tests that require the user to be logged in.

In cypress/support/commands.js you can add a custom function like this:

// Default form login
Cypress.Commands.add('loginByForm', () => {
  const username = 'testUsername'
  const password = 'testPassword'

  cy.request({
    method: 'POST',
    url: 'http://localhost:8000/login',
    body: {
      username,
      password,
    },
  })
})

Then in your tests, you can call this function before your test evaluations like the following:

/// <reference types="cypress" />

describe('Logging in using XHR request', function () {
  before(() => {
    // log in only once before any of the tests run
    cy.loginByForm()
  })

  it('can bypass the UI and still log in', function () {
    // Check for session cookie
    cy.getCookie('your-site-session-cookie').should('exist')

    // Visit the page behind the authentication
    cy.visit('/profile')
    cy.contains('h1', 'testUsername')
  })
})

Notice our tests will be much more concise the more we need the user to be authenticated.

Linting inline CSS elements

I had a website I was working on where the CMS didn’t have any CSS linting. This became problematic when there was a CSS typo saved and it made it very hard to find where the broken CSS was.

I decided to create a test where we check each inline <style></style> element. Here is what that test looks like:

/// <reference types="cypress" />
const path = require('path')

describe('Validates CSS', function () {
  // with respect to the current working folder
  const downloadsFolder = 'cypress/downloads'

  context('Style elements', function () {
    it('Has valid css in style element', () => {
      const downloadedCssFile = path.join(downloadsFolder, 'styles.css')
      
      // Go to the page needing to be checked
      cy.visit('http://localhost:8000')

      // Validate each style element as a CSS file
      cy.get('style').each(($el) => {
        cy.writeFile(downloadedCssFile, $el.text())

        cy.exec(
          `$(npm bin)/csslint ${downloadedCssFile}`, 
          { failOnNonZeroExit: false }
        ).then((obj) => {
          // Output the specific error message
          if (obj.code == 1){
            throw obj.stdout
          }
        })
      })
    })
  })
})

A few things to note with this, we need to have the csslint library installed in our project. The cy.exec() function allows us to run the CSS linter on our file we have created as though it is an existing CSS file. This means you can utilize any node package and test against any errors that the package may result in when testing.

Continuous integration

Cypress can easily be implemented in your continuous integration pipeline. In your pipeline simply add the dependencies and the commands needed for the Cypress tests.

The following is an example of a Github workflow configuration. This will run cypress every time there is a commit.

name: Automated testing

on:
  # Run automated testing on any commit.
  push:

jobs:
  run-automated-testing:
    runs-on: ubuntu-latest
    env:
      CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
    steps:
      #  Official GitHub Action used to check-out a repository so a workflow can access it.
      - uses: actions/checkout@v2

      - name: Install Node.js
        uses: actions/setup-node@v1
        with:
          node-version: '14.18.0'
      
      - name: Install Project Dependencies
        run: npm ci
      
      - name: Run Cypress tests
        run: npm run cypress:test

The npm run cypress:test line, runs the command defined in the package.json file. The key CYPRESS_RECORD_KEY must be set in Github as an “action secret”. An “action secret” is an environment secret for Github actions.

Next is an example package.json that contains the scripts for running the automated test. The script cypress:test records test results to Cypress dashboard.

{
  "scripts": {
    "cypress:run:record": "$(npm bin)/cypress run --record --key $CYPRESS_RECORD_KEY",
    "cypress:test": "start-server-and-test develop http://localhost:8000 cypress:run:record"
  }
}

The Cypress dashboard is a service that Cypress offers that is a great visual dashboard of test runs. Tests can be recorded to the dashboard from your local environment as well, you just need your dashboard key. More information on the Cypress dashboard can be accessed on the Cypress website.

You can find an example of the scripts for continuous integration in my personal website repository.

Conclusion

Although this is just scratching the surface of Cypress’s capabilities, I hope it inspires you to try it out on one of your projects. With Cypress, you can have important tests up and running in no time.


About the author