A Reliable and Developer Friendly Approach to Automated End-to-End Testing

@amirrustam

Meetup

Amir Rustamzadeh

Head of Developer Experience

@amirrustam

TREND

More teams have some automation process.

@amirrustam

Reduction of 26% to 11% from 2016 to 2017 of teams that do not automate deployment.
Source: O'Reilly & SIG

TREND

Continuous Delivery growth has been relatively small.

@amirrustam

16% in 2017 versus 11% in 2016
Source: O'Reilly & SIG

TREND

Small decline in teams

without regression testing.

@amirrustam

41% in 2017, down from 48% in 2016
Source: O'Reilly & SIG

WHY?

@amirrustam

There is large growth in automation, but relatively slower growth in delivery and testing.

TESTING IS HARD

@amirrustam

@amirrustam

Remove Friction

Faster & Better Feedback Loops

Great developer or QA experience

@amirrustam

@amirrustam

The way we write our apps changed, but our testing tools did not adapt.

What is Cypress?

A tool for reliably testing anything that runs in web browser.

Free & Open Source

MIT License

Our mission is to build a thriving, open source ecosystem that enhances productivity, makes testing an enjoyable experience, and generates developer happiness. We hold ourselves accountable to champion a testing process that actually works.

Growth in Popularity & Adoption

⭐️ 14.5K +

πŸ“¦ 550K +

GitHub Stars

NPM Weekly Downloads

Growth in Popularity & Adoption

Growth in Popularity & Adoption

Sustainable Open Source

Let's Dive In

Setup & Installation

$ npm install -D cypress
$ npm install -D cypress

Desktop App

CLI

$ npx cypress open

Open Desktop App

  • πŸ“¦ My Project

    • πŸ“ ...

    • πŸ“‚ cypress

      • πŸ“‚ fixtures

      • πŸ“‚ integration

      • πŸ“‚ plugins

      • πŸ“‚ support

Your App

Command Log

Time-Travel

API

Intuitive & English-like

API

cy.<command>

API

cy.get('button')

API

cy.get('button')
Β  .click()
  .should('have.class', 'active')

API

cy.request('/users/1')
Β  .its('body')
  .should('deep.eql',{ name:'Amir'})

API

cy.get('button')
Β  .click()
  .should('have.class', 'active')

Subject is passed

through the chain

Architecture

Backend

Browser

Your Tests

Your App

Browser Provisioning & System-level tasks

Architecture

πŸƒπŸ»β€β™‚οΈ

Faster test runs

Architecture

⚑️

Direct native Access to the DOM & your application.

Architecture

🐞

Better Debuggability

Architecture

πŸ‘Œ

Test commands are executed in aΒ deterministicΒ manner. Resulting in flake-freeΒ testing.

it('send email with contact form', () => {
    cy.get('#name-input').type('Amir')
    cy.get('#email-input').type('amir@cypress.io')
    cy.get('form').submit()
    cy.get('#success-message').should('be.visible')
})
βœ…
βœ…
βœ…
βœ…
    cy.get('#name-input').type('Amir')
    cy.get('#email-input').type('amir@cypress.io')
    cy.get('form').submit()
    cy.get('#success-message').should('be.visible')

Architecture

⏳

Automatic Waiting

API

cy.get('button')
Β  .click()
  .should('have.class', 'active')

Cypress will automatically wait for this assertion (4 seconds by default)

Let's dive deeper

🐦 BirdBoard

The Twitter client no one needs

🐦 BirdBoard

🐦 BirdBoard

it('signup and login user', () => {
  cy.visit('http://localhost:8080/signup')

  cy.get('input[name="email"]').type('amir@cypress.io')
  cy.get('input[name="password"]').type('1234')
  cy.get('input[name="confirm-password"]').type('1234')
  cy.get('#signup-button').click()

  cy.location('pathname').should('eq', '/login')

  cy.get('input[name="email"]').type('amir@cypress.io')
  cy.get('input[name="password"]').type('1234')
  cy.get('#login-button').click()

  cy.location('pathname').should('eq', '/board')
})

API

cy.task

Execute JS on the system (outside of the browser)

// cypress/plugins/index.js

const { clearDatabase } = require('../../server/db')

module.exports = (on, config) => {
  on('task', {
    'clear:db': () => {
      return clearDatabase()
    }
  })
}
context('User setup', () => {
  beforeEach(() => {
    cy.task('clear:db')
  })

  it('signup and login user', () => {
    cy.visit('http://localhost:8080/signup')

    cy.get('input[name="email"]').type('amir@cypress.io')
    cy.get('input[name="password"]').type('1234')
    cy.get('input[name="confirm-password"]').type('1234')
    cy.get('#signup-button').click()

    cy.location('pathname').should('eq', '/login')

    cy.get('input[name="email"]').type('amir@cypress.io')
    cy.get('input[name="password"]').type('1234')
    cy.get('#login-button').click()

    cy.location('pathname').should('eq', '/board')
  })
})
context('User setup', () => {
  beforeEach(() => {
    cy.task('clear:db')
  })

  it('signup and login user', () => {
    cy.visit('http://localhost:8080/signup')

    cy.get('input[name="email"]').type('amir@cypress.io')
    cy.get('input[name="password"]').type('1234')
    cy.get('input[name="confirm-password"]').type('1234')
    cy.get('#signup-button').click()

    cy.location('pathname').should('eq', '/login')

    cy.get('input[name="email"]').type('amir@cypress.io')
    cy.get('input[name="password"]').type('1234')
    cy.get('#login-button').click()

    cy.location('pathname').should('eq', '/board')
  })
})
context('User setup', () => {
  beforeEach(() => {
    cy.task('clear:db')
  })

  it('signup and login user', () => {
    cy.visit('http://localhost:8080/signup')

    cy.get('input[name="email"]').type('amir@cypress.io')
    cy.get('input[name="password"]').type('1234')
    cy.get('input[name="confirm-password"]').type('1234')
    cy.get('#signup-button').click()

    cy.location('pathname').should('eq', '/login')

    cy.login('amir@cypress.io', '1234')

    cy.location('pathname').should('eq', '/board')
  })
})
// cypress/support/commands.js

Cypress.Commands.add('login', (email, password) => {
  cy.get('input[name="email"]').type(email)
  cy.get('input[name="password"]').type(password)
  cy.get('#login-button').click()
})

Custom Commands

context('User setup', () => {
  beforeEach(() => {
    cy.task('clear:db')
  })

  it('signup and login user', () => {
    cy.visit('http://localhost:8080/signup')

    cy.get('input[name="email"]').type('amir@cypress.io')
    cy.get('input[name="password"]').type('1234')
    cy.get('input[name="confirm-password"]').type('1234')
    cy.get('#signup-button').click()

    cy.location('pathname').should('eq', '/login')

    cy.login('amir@cypress.io', '1234')

    cy.location('pathname').should('eq', '/board')
  })
})
// cypress/plugins/index.js

const { clearDatabase, seedDatabase } = require('../../server/db')

module.exports = (on, config) => {
  on('task', {
    'clear:db': () => {
      return clearDatabase()
    }
  })

  on('task', {
    'seed:db': (data) => {
      return seedDatabase(data)
    }
  })
}
const userSeed = require('../../server/seed/users')

context('User setup', () => {
  beforeEach(() => {
    cy.task('clear:db')
    cy.task('seed:db', userSeed.data)
  })

  it('login user', () => {
    cy.visit('http://localhost:8080/login')

    cy.login('amir@cypress.io', '1234')

    cy.location('pathname').should('eq', '/board')
  })
})
// cypress/support/commands.js

Cypress.Commands.add('loginWithUI', (email, password) => {
  cy.get('input[name="email"]').type(email)
  cy.get('input[name="password"]').type(password)
  cy.get('#login-button').click()
})

Cypress.Commands.add('login', (email, password) => {
  return cy.window().then(win => {
    return win.app.$store.dispatch('login', {
      email: 'amir@cypress.io',
      password: '1234'
    })
  })
})

Login Custom Command

const userSeed = require('../../server/seed/users')

context('BirdBoard', () => {
  beforeEach(() => {
    cy.task('clear:db')
    cy.task('seed:db', userSeed.data)

    cy.visit('http://localhost:8080/login')

    cy.login('amir@cypress.io', '1234')
  })

  it('load tweets for selected hashtags', () => {
    cy.server()

    // Fixture is stored in cypress/fixtures/tweets.json
    cy.route('GET', '/tweets*', 'fixture:tweets')
      .as('tweets')

    cy.get('#hashtags')
      .type('javascript{enter}')
      .type('cypressio{enter}')

    cy.window().then(win => {
      cy.wait('@tweets')
        .its('response.body.tweets')
        .should('have.length', win.app.$store.state.tweets.length)
    })
  })
})

Stubbing Network Response with Fixtures

Stubbing Network Response with Fixtures

const userSeed = require('../../server/seed/users')

context('BirdBoard', () => {
   // ....

  it('load tweets for selected hashtags', () => {
    cy.server()

    // Fixture is stored in cypress/fixtures/tweets.json
    cy.fixture('tweets').then((tweets) => {
      cy.route({
        url: '/tweets*',
        response: tweets,
        delay: 3000, // simulate slow response
        status: 404  // simulate error scenarios 
      })
      .as('tweets')
    })

    cy.get('#hashtags')
      .type('javascript{enter}')
      .type('cypressio{enter}')

    cy.window().then(win => {
      cy.wait('@tweets')
        .its('response.body.tweets')
        .should('have.length', win.app.$store.state.tweets.length)
    })
  })
})

Stubbing Network Response with Fixtures

How do you run Cypress in CI?

Headless Mode

$ npx cypress run

Test Run Video Recording

Out-of-the-Box

$ npx cypress run

Record Results to

Cypress Dashboard

$ npx cypress run --record

Cypress Dashboard

Test Run Status & Performance Details

Organized & Shareable Error Reports

Cypress Dashboard

Optimize CI Usage with Parallelization

$ npx cypress run --record --parallel

Upcoming

Test Retries

Reduce pipeline failures and identify true flaky tests.

βœ… 999

❌ 1

Upcoming

Β 

Full-Network Stubbing

Β 

Improved Error Reporting

Β 

Cross-browser Support

Docs

πŸ“š docs.cypress.io

Example Recipes

@cypress/code-coverage

Plugins, Plugins, Plugins

πŸ— Component Testing

πŸ“Έ Visual Regression

♿️ Accessibility Testing

✨ ...and more ✨

🀝

Community

Gitter

on.cypress.io/chat

πŸŽ“

Β 

Workshop Tomorrow

Thank You

Happy Testing

Amir Rustamzadeh

Head of Developer Experience

@amirrustam