End-to-End Testing with Cypress

"Cypress can test anything that runs in a browser."

main site: http://cypress.io

guides: https://docs.cypress.io/guides

API doc: https://docs.cypress.io/api

Cypress Features

Cypress Anti-Features

Cypress is not an after-the-fact click-and-record QA automation tool for so-called "non-technical" testers.

"Yes, this may require server side updates, but you have to make an untestable app testable if you want to test it!" - Cypress Conditional Testing Guide

Cypress Runner UI

launches with cypress open

cypress runner

Cypress is also runnable "headless" with cypress run

Waiting

  1. you use assertions to tell Cypress what the desired state of your application should be
  2. Cypress will automatically wait for your application to reach this state before moving on

Simple Example

describe('Post Resource', function() {
  it('Creating a New Post', function() {
    cy.visit('/posts/new')     // 1.

    cy.get('input.post-title') // 2.
      .type('My First Post')   // 3.

    cy.get('input.post-body')  // 4.
      .type('Hello, world!')   // 5.

    cy.contains('Submit')      // 6.
      .click()                 // 7.

    cy.url()                   // 8.
      .should('include', '/posts/my-first-post')

    cy.get('h1')               // 9.
      .should('contain', 'My First Post')
  })
})
  1. Visit the page at /posts/new.
  2. Find the <input> with class post-title.
  3. Type "My First Post" into it.
  4. Find the <input> with class post-body.
  5. Type "Hello, world!" into it.
  6. Find the element containing the text "Submit".
  7. Click it.
  8. Grab the browser URL & ensure it includes /posts/my-first-post.
  9. Find the h1 tag & ensure it contains the text "My First Post".

from https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Cypress-Is-Simple

visit

cy.visit('/cart')

{
    "baseUrl": "http://localhost:5000/"
}

get

cy.get('#someId')

cy.get('form#login input[name="username"]')
  .type('HomerSimpson1989')
cy.get('form#login')
  .submit()

Note: cypress Chainers are quite similar to jQuery wrapper objects. Both are DOM element collection wrappers that support method chaining.

some commands for interacting with the DOM

see https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html for more info -- there are many details here

contains

e.g.:

cy.get('h2')
  .contains('New York')

built-in assertions

every Cypress command has a built-in assertion related to its purpose

the command keeps checking many times a second, waiting for the assertion to become true

For instance:

https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Default-Assertions

should

Sometimes the built-in assertions are enough, but often you need to test the page's contents in other ways.

Cypress's should method lets you use Chai assertions on the element(s) matched by get.

Note: Chai assertions are slightly different from Jest assertions, so beware of small syntax differences.

Using should on a chainer, you specify the Chai assertion as a string; should will execute that assertion repeatedly on the target element until it becomes true.

cy.get('input[name="firstName"]')
  .should('have.value', 'Homer')

Here’s a list of commonly used Cypress assertions, with sample code: https://docs.cypress.io/guides/references/assertions.html#Common-Assertions

Note: it is weird that some methods are called normally, and some are called by passing a string as a parameter to should. This is due to a technical detail about how Cypress delegates to Chai, and hopefully isn't too confusing. When in doubt, look at working code examples and follow their lead.

nothing happens immediately

Cypress commands don’t do anything at the moment they are invoked, but rather enqueue themselves to be run later...

... after the entire test function has already finished executing!

cy.get returns a wrapper object called a "chainer", and at the time it is returned, nothing in the web page has happened yet, so you can't simply store the result in a variable or print it

(For that level of control you must pass callbacks into other methods like should and each and and and then.)

This may seem overcomplicated, but it is by design. Commands are enqueued and managed by Cypress to reduce timing issues and general test flakiness.

timeout

Most commands expire after 4 seconds. This "timeout" causes the command and test to fail.

Some commands have longer default timeouts -- e.g. visit's is 60 seconds, to account for long page load and app startup times.

Any timeout can be overridden temporarily with an option, e.g.:

cy
  .get('.mobile-nav', { timeout: 10000 }) // 10 seconds
  .should('be.visible')

then

cy.get('div#preview').then((element) => {
  assert.include(element.text().toLowerCase(), 'hello');
});

multiple matches

For example, given this HTML:

<h2>New York</h2>
<h2>Los Angeles</h2>
cypress code result
cy.get('h2')
.contains('New York') OK: one success
.contains('York') OK: one success
.should('have.text', 'New York') Failure: YorkLos
.then((element) => {
   expect(element.text()).to.equal('New York')
});
Failure: YorkLos

checking multiple matching elements with each

Fortunately, there is each

cypress code result
cy.get('h2')
  .each((element) => {
     element.text()
     .should.equal('New York');
});
One failure, one OK: one success, one failure

project structure

    describe('Unit test our math functions', function() {
      context('math', function() {
        it('can add numbers', function() {
          expect(add(1, 2)).to.eq(3)
        })
      })
     })

see https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Folder-Structure

only

If you want to temporarily enable or disable tests, you can use only or skip, e.g.

when these tests are run, all will be skipped except this one:

it.only('returns "fizz" when number is multiple of 3', function () {

skip is the inverse of only, temporarily removing a single test case from the running suite so it doesn't clutter your logs.

it.skip('does something interesting that is not coded yet', function () {

Remember to delete your skips and onlys before making a commit!

debug

Sometimes an error message is not enough, and you need to pause and inspect the DOM, or other application state.

Cypress saves DOM snapshots for every command, so normally all you need to do is click the line in the log panel, then inspect the window visually or with Chrome DevTools.

If you want more fine-grained breakpoints...

Use debugger or .debug() just BEFORE the action:

// break on a debugger before the click command
cy.get('button').debug().click()
// break on a debugger before the get command
debugger
cy.get('button').click()

Remember to delete your debugs before making a commit!

LAB: Tic Tac Test (part 1)

git clone git@github.com:BurlingtonCodeAcademy/tic-tac-toe-jon-and-bob.git
cd tic-tac-toe-jon-and-bob
npm install cypress
npm install node-static
{
    "baseUrl": "http://localhost:5000/"
}

mkdir -p cypress/integration
describe('Cypress', function () {
  it('successfully visits the home page', function () {
    cy.visit('/');
  });
});

LAB: Tic Tac Test (part 2)

npx node-static .
npx cypress open

Even More Cypress Stuff

code editor integration

At the top of your Cypress test file, if you include the following line...

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

...then your text editor will become aware of the types and interfaces of the Cypress library, and enable code completion and inline help.

This trick works in Visual Studio Code (and probably other editors too)

alias

alias a selection with as for later use (since local variables are awkward to use in an async world)

cy.get('table').find('tr').as('rows');
cy.get('@rows').first().click();

stubs

stubs and spies, mocks and clocks for when you want to deceive your app

cy.clock()
cy.get('#timer').contains('00:00.00')
cy.get('#start').click()
cy.tick(10 * 60 * 1000)
cy.get('#timer').contains('10:00.00')

fixtures and routes

cy.server()           // enable response stubbing
cy.route({
  method: 'GET',      // Route all GET requests
  url: '/users/*',    // that have a URL that matches '/users/*'
  response: []        // and force the response to be: []
})
cy.fixture('activities.json').as('activitiesJSON')
cy.route('GET', 'activities/*', '@activitiesJSON')

docs

Great docs! Including a Recipies cheatsheet and clearly written guides

 Previous Lesson Next Lesson 

Outline

[menu]

/