Quickstart into End to End Testing with Cypress.io

Installation

Change into your project root folder (package.json must exist) and run

yarn add cypress -dev

Verify that it was correctly installed by running Cypress:

yarn run cypress open

or in case this makes trouble (it did for me, see below) run that instead:

node_modules\.bin\cypress open

For convenience you can add a script to package.json:

{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run"
  }
}

The official docs provide further info about installation such as

  • Listing system requirements
  • Installing Cypress on Ubuntu/Debian, CentOS, Docker
  • Installing Cypress via direct download
  • Installing a version different than the default npm package (including pre-release versions)
  • Changing environment variables
  • Downloading specific versions for specific platforms via URL

Troubleshooting Installation

Right after installation when trying to start cypress with yarn run cypress open I was greeted with an error:

It looks like this is your first time using Cypress: 8.5.0


Cypress failed to start.

This may be due to a missing library or dependency.
https://on.cypress.io/required-dependencies

Please refer to the error below for more details.

----------

Command failed with exit code 2147483651: C:\Users\myusername\AppData\Local\Cypress\Cache\8.5.0\Cypress\Cypress.exe --smoke-test --ping=61

----------

Platform: win32 (10.0.18363)
Cypress Version: 8.5.0

What I did to fix it was

.\\node_modules\\.bin\\cypress.cmd install --force

and then instead of yarn run cypress open, I had to run it like this to make it work:

node_modules\.bin\cypress open

What you can do with Cypress

  • Write End-to-End tests in BDD (expect/should) and TDD (assert) assertion styles
  • Write unit tests
  • Test react components

Important Cypress concepts

  • Cypress is built on top of Mocha, Sinon and Chai.
  • Cypress Test Runner runs all your tests in a real browser installed on your local system
  • Cypress commands don’t do anything at the moment they are invoked, but rather enqueue themselves to be run later in serial order
  • Cypress commands are asynchronous but you should not/can’t use async/await or loops

Folder structure

The first time you run cypress in your project it will create a default folder structure in your project root:

/cypress
  - /fixtures (external pieces of static data that can be used by your tests)
  - /integration
    - /examples
  - /plugins (executes in Node before the project is loaded, before the browser launches, and during test execution)
  - /support/index.js (runs before every single spec file, except on "Run all specs". Good to place global functions here)

The following folders are created in certain situations:

/cypress
  - /downloads (when a test downloads a file)
  - /screenshots (via cy.screenshot() or if test fails)
  - /videos

You should add screenshots and videos to your .gitignore.

Writing tests

Tests can be written in .js, .jsx, .coffee and .cjsx.

Create a test file mytest.spec.js in my-app/cypress/integration.

// mytest.spec.js

describe('My test suite', () => {
  it('should test something', () => {

  })
})

context() is identical to describe() and specify() is identical to it().

Test configuration

You can configure things such a baseUrl, browser, requestTimeout, retries and many more.

describe(name, config, fn)
context(name, config, fn)
it(name, config, fn)
specify(name, config, fn)

Querying elements by CSS selectors

cy.get('.my-selector');
cy.get('#elementId');
cy.get(':checkbox')
// and all other possible CSS selectors

Adjusting async retry logic timeout

Cypress wraps all DOM queries with an asynchronous retry-and-timeout logic. That’s why the following will not work:

// This will not work! Cypress does not return the element synchronously.
const $cyElement = cy.get('.element')

Default timeouts

  • 4 seconds for selecting elements
  • 6 seconds for cy.visit() and cy.exec()
  • cy.wait() 5 seconds when waiting for routing alias plus 3 seconds for the server response

You can define your own timeout if an element needs more time to appear:

// Give this element 10 seconds to appear
cy.get('.my-slow-selector', { timeout: 10000 })

Here is an example with two assertions:

cy.get('.mobile-nav').should('be.visible').and('contain', 'Home')

It is important to understand that the default timeout of 4 seconds applies to the command, not the assertions. In other words: Cypress gives a total of 4 seconds to assert that the element .mobile-nav is visible and contains the text Home, and not a total of 4 + 4 + 4 = 12 seconds.

You can also set a global timeout.

Access a selected element

cy.get('#some-link')
  .then(($myElement) => {
    // grab its href property
    const href = $myElement.prop('href')
    // strip out the 'hash' character and everything after it
    return href.replace(/(#.*)/, '')
  })
  .then((href) => {
    // href is now the new subject
    // which we can work with now
  })

Querying elements by text content

// Find an element in the document containing the text 'New Post'
cy.contains('New Post')

// Find an element within '.main' containing the text 'New Post'
cy.get('.main').contains('New Post')

Instead of cy.get(selector).should('contain', text) or cy.get(selector).contains(text) chain, it is recommended using cy.contains(selector, text) which is retried automatically as a single command.

You probably do not want to use this approach if your text changes frequently or you use translations.

Filter elements

cy.get('td').filter('.users')

Interacting with elements

Cypress will wait until a selected element becomes “actionable”, e.g. waits until is not hidden, not being covered, not being disabled, not being animated.

cy.get('textarea.post-body').type('This is an excellent post.')

// others:
.blur()
.focus()
.clear()
.check()
.uncheck()
.select()
.dblclick()
.rightclick()

Example of pressing keys (e.g. Enter) into an element:

cy.get('.new-todo').type('todo A{enter}')

An events’ coordinates are fired at the center of the element, but most commands enable you to change the position it’s fired to.

cy.get('button').click({ position: 'topLeft' })

Debugging interactions

cy.get('button').debug().click()

Now open up the dev tools in the test runner and debug from there.

In the web dev console you have access to subject variable which points to the selected element. You can invoke functions, e.g. subject.text().

Assertions

Cypress uses Chai to handle assertions. Here is the full assertion reference.

Cypress will automatically wait until these assertions pass. This prevents you from having to know or care about the precise moment your elements eventually do reach this state.

cy.get(':checkbox').should('be.disabled')

// others:
.should('have.class', 'form-horizontal')
.should('not.have.value', 'US')

Assert existence of an element

.should('exist')
.should('not.exist')
.should('be.visible')
.should('not.be.visible')

Example of asserting that elements disappear after an action:

// now Cypress will wait until this
// <button> is not in the DOM after the click
cy.get('button.close').click().should('not.exist')

// and now make sure this #modal does not exist in the DOM
// and automatically wait until it's gone!
cy.get('#modal').should('not.exist')

Assert if CSS class or CSS property exists

.should('have.class', 'my-list-item')
.should('have.css', 'background-color', 'blue')

Example of asserting that an object contains a specific path of properties:

cy.window()
  .its('app.model.todos') // retried
  .should('have.length', 2)

Assert text content

.invoke('text')
  .should('contain')
  .should('not.contain')

Default assertions

With Cypress, you don’t have to assert to have a useful test. This is because many commands have a built in Default Assertion which offer you a high level of guarantee. For example cy.visit(‘http://somesite.org’) expects the page to send text/html content with a 200 status, or that cy.request() expects that a server provides a response etc.

A full list of assertion can be found in this reference.

Chaining multiple assertions

cy.get('#header a')
  .should('have.class', 'active')
  .and('have.attr', 'href', '/users')

or use the long way – if you want to change the element in some way prior to making the assertion:

cy.get('tbody tr:first').should(($tr) => {
  expect($tr).to.have.class('active')
  expect($tr).to.have.attr('href', '/users')
})

Here is another advanced example in which we assert that all p-elements have a specific text:

cy.get('p').should(($p) => {
  // massage our subject from a DOM element
  // into an array of texts from all of the p's
  let texts = $p.map((i, el) => {
    return Cypress.$(el).text()
  })

  // jQuery map returns jQuery object
  // and .get() converts this to an array
  texts = texts.get()

  // array should have length of 3
  expect(texts).to.have.length(3)

  // with this specific content
  expect(texts).to.deep.eq([
    'Some text from first p',
    'More text from second p',
    'And even more text from third p',
  ])
})

You have to make sure that the entire function can be executed multiple times without side effects.

Best practice: Alternate commands and assertions

it('adds two items', () => {
  cy.visit('/')

  cy.get('.new-todo').type('todo A{enter}')
  cy.get('.todo-list li') // command
    .should('have.length', 1) // assertion
    .find('label') // command
    .should('contain', 'todo A') // assertion

  cy.get('.new-todo').type('todo B{enter}')
  cy.get('.todo-list li') // command
    .should('have.length', 2) // assertion
    .find('label') // command
    .should('contain', 'todo B') // assertion
})

Aliases

Instead of selecting an element by writing selectors such as '.my-selector > p.something' many times you can instead define a short alias for convenience.

cy.get('.my-selector')
  .as('myElement') // sets the alias
  .click()

/* many more actions */

cy.get('@myElement') // re-queries the DOM as before (only if necessary)
  .click()

It is recommended that you alias elements as soon as possible instead of further down a chain of commands.

Aliases when using Mocha hooks

beforeEach(() => {
  // alias the $btn.text() as 'text'
  cy.get('button').invoke('text').as('text')
})

it('has access to text', function () {
  this.text // access is synchronously, but cannot use with arrow function
  cy.get('@text') // access is asynchronously and also we can use arrow functions
})

Triggering events on selected elements / Dynamically generate tests

You use .trigger():

describe('if your app uses jQuery', () => {
  ;['mouseover', 'mouseout', 'mouseenter', 'mouseleave'].forEach((event) => {
    it('triggers event: ' + event, () => {
      // if your app uses jQuery, then we can trigger a jQuery
      // event that causes the event callback to fire
      cy.get('#with-jquery')
        .invoke('trigger', event)
        .get('#messages')
        .should('contain', 'the event ' + event + 'was fired')
    })
  })
})

Fixtures

beforeEach(() => {
  // alias the users fixtures
  cy.fixture('users.json').as('users')
})

it('utilize users in some way', function () {
  // use the special '@' syntax to access aliases
  // which avoids the use of 'this'
  cy.get('@users').then((users) => {
    // access the users argument
    const user = users[0]

    // make sure the header contains the first
    // user's name
    cy.get('header').should('contain', user.name)
  })
})

Running Tests

  • Cypress suggests running test files individually by clicking on the spec filename to ensure the best performance.
  • But Cypress also allows you to run all spec files together by clicking the “Run all specs” button. This mode is equivalent to concatenating all spec files together into a single piece of test code.
  • You can also run a subset of all specs by entering a text search filter.

Watching for changes

When running in using cypress open, Cypress watches the filesystem for changes to your spec files. Soon after adding or updating a test Cypress will reload it and run all of the tests in that spec file.

Skipping tests

Can be used as placeholder, when you have not yet completely written the test. Those tests will have the state pending.

describe('TodoMVC', () => {
  it('is not written yet')

  it.skip('adds 2 todos', function () {
    cy.visit('/')
    cy.get('.new-todo').type('learn testing{enter}').type('be cool{enter}')
    cy.get('.todo-list li').should('have.length', 100)
  })

  xit('another test', () => {
    expect(false).to.true
  })
})

Run in debug mode

DEBUG=cypress:server:specs npx cypress open
## or
DEBUG=cypress:server:specs npx cypress run

Conditional Testing

The only way to do conditional testing on the DOM is if you are 100% sure that the state has “settled” and there is no possible way for it to change.

In all other cases you can still achieve conditional testing without relying on the DOM. You have to anchor yourself to another piece of truth that is not mutable.

To write tests for A/B Testing you could program your server that always returns a fixed state (A or B) depending on a parameter that you send along:

cy.visit('https://app.com?campaign=A')

cy.visit('https://app.com?campaign=B')

Alternatively, if your server saves the campaign with a session, you could ask your server to tell you which campaign you are on.

// this sends us the session cookies
cy.visit('https://app.com')

// assuming this sends us back
// the campaign information
cy.request('https://app.com/me')
  .its('body.campaign')
  .then((campaign) => {
    // runs different cypress test code
    // based on the type of campaign
    return campaigns.test(campaign)
  })

Another way to test this is if your server sent the campaign in a session cookie that you could read off.

cy.visit('https://app.com')
cy.getCookie('campaign').then((campaign) => {
  return campaigns.test(campaign)
})

Another valid strategy would be to embed data directly into the DOM – but do so in a way where this data is always present and query-able. It would have to be present 100% of the time, else this would not work.

cy.get('html')
  .should('have.attr', 'data-campaign')
  .then((campaign) => {
    return campaigns.test(campaign)
  })

The official doc has more strategies of conditional testing.

Environment variables

Option 1: Setting env var via command line

E.g. export CYPRESS_MY_VARIABLE="hello" on Linux (must start with CYPRESS_).

Option 2: Passing as argument

cypress open --env MY_VARIABLE="hello"

Option 3: Adding to Cypress.json

{
  "env": {
    "MY_ENV_VARIABLE" : "hello"
  }
}

Option 4: Create cypress.env.json

{
  "MY_ENV_VARIABLE" : "hello"
}

Accessing env var in tests

const my = Cypress.env('MY_VARIABLE')

Remember to not commit secret info to your repository.

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.