Cypress for Angular Developers

Gleb Bahmutov

Sr Director of Engineering

Ex-Distinguished Engineer

EX-VP of Engineering

Cypress.io 

Sli.do event code #cy-for-ng

our planet is in imminent danger

https://lizkeogh.com/2019/07/02/off-the-charts/

+3 degrees Celsius will be the end.

survival is possible* but we need to act now

  • change your life
  • dump banks financing fossil projects
  • join an organization

Agenda

  • Protractor Cypress for E2E
  • 📃 objects is ✅
  • Mocks and fixtures
  • Component testing
  • Q & A

Sli.do event code #cy-for-ng

Speaker: Gleb Bahmutov PhD

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional

(these slides)

"Run digest cycle in web worker" ng-conf 2015

angular.module('A', []).value('foo', 42);
ngDescribe({
  name: 'inject example',
  modules: 'A',
  inject: ['foo', '$timeout'],
  tests: function (deps) {
    it('has foo', function () {
      expect(deps.foo).toEqual(42);
    });
    it('has timeout service', function () {
      expect(typeof deps.$timeout).toEqual('function');
    });
  }
});

Make testing (Angular) apps simpler

50 people. Atlanta, Philly, Boston, NYC, the world

Fast, easy and reliable testing for anything that runs in a browser

$ npm install -D cypress
it('adds 2 todos', () => {
  cy.visit('http://localhost:3000')
  cy.get('.new-todo')
    .type('learn testing{enter}')
    .type('be cool{enter}')
  cy.get('.todo-list li')
    .should('have.length', 2)
})
$ npx cypress open

Wait, where is the _Angular_ testing?

Cypress is framework-agnostic, its knowledge is very portable

Time to migrate away from Protractor

  • Reliable tests
  • Unofficial testing choice via Nx
  • Community has spoken
  • Every Angular conference had Gleb show & teach Cypress

Cypress for Ng

🕵🏻‍♂️ 🎁

Schematic

$ ng add @cypress/schematic

Copied from briebug/cypress-schematic and maintained by Cypress team

$ ng run {project-name}:cypress-open
$ ng run {project-name}:cypress-run
# set up E2E with Cypress
$ ng e2e
describe('Authorization tests', () => {
  it('allows the user to signup for a new account', () => {
    browser.get('/signup')
    element(by.css('#email-field')).sendKeys('user@email.com')
    element(by.css('#confirm-email-field')).sendKeys('user@email.com')
    element(by.css('#password-field')).sendKeys('testPassword1234')
    element(by.cssContainingText('button', 'Create new account')).click()

    expect(browser.getCurrentUrl()).toEqual('/signup/success')
  })
})

Protractor

describe('Authorization Tests', () => {
  it('allows the user to signup for a new account', () => {
    cy.visit('/signup')
    cy.get('#email-field').type('user@email.com')
    cy.get('#confirm-email-field').type('user@email.com')
    cy.get('#password-field').type('testPassword1234')
    cy.get('button').contains('Create new account').click()

    cy.url().should('include', '/signup/success')
  })
})

Cypress

element(by.tagName('h1'))
element(by.css('.my-class'))
element(by.id('my-id'))
element(by.name('field-name'))
element(by.cssContainingText('.my-class', 'text'))
element(by.linkText('text'))

Protractor

cy.get('h1')
cy.get('.my-class')
cy.get('#my-id')
cy.get('input[name="field-name"]')
cy.get('.my-class').contains('text')
cy.contains('text')

Cypress

Selecting Elements

💎 Selector Playground

💎💎💎 Cypress Studio

element(by.css('button')).click()
element(by.css('input')).sendKeys('my text')
element(by.css('input')).clear()
element.all(by.css('[type="checkbox"]')).first().click()
element(by.css('[type="radio"][value="radio1"]')).click()
element.all(by.css('[type="checkbox"][checked="true"]')).first().click()
element(by.cssContainingText('option', 'my value')).click()

Protractor

cy.get('button').click()
cy.get('input').type('my text')
cy.get('input').clear()
cy.get('[type="checkbox"]').first().check()
cy.get('[type="radio"]').check('radio1')
cy.get('[type="checkbox"]').not('[disabled]').first().uncheck()
cy.get('select[name="optionsList"]').select('my value')

Cypress

Interacting with Elements

const list = element.all(by.css('li.selected'))
expect(list.count()).toBe(3)
expect(
  element(by.tagName('form'))
  .element(by.tagName('input'))
  .getAttribute('class')
).not.toContain('disabled')

Protractor

cy.get('li.selected').should('have.length', 3)
cy.get('form').find('input')
  .should('not.have.class', 'disabled')

Cypress

Assertions

Is there an automated way to convert Protractor tests to Cypress specs?

No, sorry.

Migration Strategy

  • Watch "How Siemens SW Hub increased their test productivity by 38% with Cypress" webinar
  • Do not try to rewrite 100% of the tests at once 🤕
  • Replace the most important tests first 🎯
  • Keep replacing the tests as you touch those features 📈

Use Code Coverage

  • need to add tests for adding a hero and deleting a hero

What About Page Objects?

const page = {
  login: () => {
    element(by.css('.username')).sendKeys('my username')
    element(by.css('.password')).sendKeys('my password')
    element(by.css('button')).click()
  },
}

it('should display the username of a logged in user', () => {
  page.login()
  expect(by.css('.username').getText()).toEqual('my username')
})

Protractor

What About Page Objects?

const page = {
  login () {
    cy.get('.username').type('my username')
    cy.get('.password').type('my password')
    cy.get('button').click()
  },
}

it('should display the username of a logged in user', () => {
  page.login()
  cy.get('.username').contains('my username')
})

Cypress

What About Page Objects?

Cypress.Commands.add('login', (username, password) => {
  cy.get('.username').type(username)
  cy.get('.password').type(password)
  cy.get('button').click()
})

it('should display the username of a logged in user', () => {
  cy.login('Matt', Cypress.env('password'))
  cy.get('.username').contains('Matt')
})

Cypress

Interact With Your App

export class HeroListComponent implements OnInit {
  heroes: Observable<Hero[]>;
  selectedHero: Hero;

  constructor(private router: Router, private heroService: HeroService) {
    // @ts-ignore
    if (window.Cypress) {
      // @ts-ignore
      window.heroService = this.heroService;
    }
  }

Interact With Your App

it('should delete a hero', () => {
  cy.window()
    .its('heroService')
    .invoke('deleteHero', { id: 11 })
    .invoke('subscribe', (result) => {
      // TODO return a promise
      // so Cypress can wait for 
      // this event
      expect(result).to.equal(null);
    });
});

App Actions

it('deletes all heroes through UI', () => {
  cy.visit('/heroes')
  // confirm the heroes have loaded and select "delete" buttons
  cy.get('ul.heroes li button.delete')
    .should('have.length.gt', 0)
    // and delete all heroes
    .click({ multiple: true })
})

App Actions

it('deletes all heroes through UI', () => {
  cy.visit('/heroes')
  // confirm the heroes have loaded and select "delete" buttons
  cy.get('ul.heroes li button.delete')
    .should('have.length.gt', 0)
    // and delete all heroes
    .click({ multiple: true })
})

App Actions

const getHeroesComponent = () =>
  cy.window()
    .should('have.property', 'HeroesComponent')

const getHeroes = () =>
  getHeroesComponent().should('have.property', 'heroes')

const clearHeroes = () =>
  getHeroes()
    .then(heroes => {
      cy.log(`clearing ${heroes.length} heroes`)
      // @ts-ignore
      heroes.length = 0
    })

it('deletes all heroes through app action', () => {
  cy.visit('/heroes')
  clearHeroes()
  getHeroes().should('have.length.gt', 0)
})

App Actions

Use Network Mocking

it('should delete a hero', () => {
  cy.intercept('DELETE', '/api/heroes/11').as('sad')
  cy.window()
    .its('heroService')
    .invoke('deleteHero', { id: 11 })
  cy.wait('@sad')
});

Cypress tests can control the network responses

The intercept  architecture

Intercept HTTP calls here

Any request can be observed or stubbed

(+ ServiceWorker, WebWorker)

cy.intercept( routeMatcher )

Spy on requests matching the route

cy.intercept( routeMatcher, response )

Stub requests matching the route

cy.intercept(...).as('alias')

Give request an alias for waiting

General form

cy.intercept(url, routeHandler?)
cy.intercept(method, url, routeHandler?)
cy.intercept(routeMatcher, routeHandler?)

if present, than it is a stub*

*mostly

cy.intercept(url, routeHandler?)
cy.intercept(method, url, routeHandler?)
cy.intercept(routeMatcher, routeHandler?)
cy.intercept('http://example.com/widgets') // spy
cy.intercept('http://example.com/widgets', 
             { fixture: 'widgets.json' })  // stubs
cy.intercept('POST', 'http://example.com/widgets', 
             { statusCode: 200, body: 'it worked!' })
cy.intercept({ method: 'POST', 
               url: 'http://example.com/widgets' }, 
             { statusCode: 200, body: 'it worked!' })

cy.intercept super power

Cypress

route handler can be programmatic

cy.intercept('POST', '/graphql', (req) => {
  if (req.body.hasOwnProperty('mutation')) {
    req.alias = 'gqlMutation'
  }
})

// assert that a matching request has been made
cy.wait('@gqlMutation')

Spy on some requests

*routeHandler but is a spy

cy.intercept('POST', '/graphql', (req) => {
  if (req.body.hasOwnProperty('mutation')) {
    req.reply({
      data: {
        id: 101
      }
    })
  }
})

if you call req.reply from the route handler, it becomes a stub*

Stub some requests

cy.intercept('POST', '/graphql', (req) => {
  if (req.body.hasOwnProperty('mutation')) {
    req.reply((res) => {
      // 'res' represents the real destination's response
      // three items in the response is enough
      res.data.items.length = 3
    })
  }
})

Change server's response

How cy.intercept works

// requests to '/users.json' will be fulfilled
// with the contents of the "users.json" fixture
cy.intercept('/users.json', 
             { fixture: 'users.json' })

Fixtures

cy.fixture('users').then(users => {
  // do something with the list
})

load JSON, images, binary files using https://on.cypress.io/fixture

cy.fixture('init-array').then(initArray => {
  cy.fixture('solved-array').then(solvedArray => {
    cy.stub(UniqueSudoku, 'getUniqueSudoku')
      .returns([initArray, solvedArray])
  })
})

Pyramid of Fixtures

import initArray from '../cypress/fixtures/init-array.json'
import solvedArray from '../cypress/fixtures/solved-array.json'

it('mocks board creation', () => {
  cy.stub(UniqueSudoku, 'getUniqueSudoku')
    .returns([initArray, solvedArray])
  ...
})

Angular Component Testing

import { initEnv, mount } from 'cypress-angular-unit-test';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  it('shows the input', () => {
    // Init Angular stuff
    initEnv(AppComponent);
    // You can also :
    // initEnv({declarations: [AppComponent]});
    // initEnv({imports: [MyModule]});

    // component + any inputs object
    mount(AppComponent, { title: 'World' });
    // use any Cypress command afterwards
    cy.contains('Welcome to World!');
  });
});

Angular Component Testing

import { initEnv, mount } from 'cypress-angular-unit-test';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  it('shows the input', () => {
    // Init Angular stuff
    initEnv(AppComponent);
    // You can also :
    // initEnv({declarations: [AppComponent]});
    // initEnv({imports: [MyModule]});

    // component + any inputs object
    mount(AppComponent, { title: 'World' });
    // use any Cypress command afterwards
    cy.contains('Welcome to World!');
  });
});

Angular Component Testing

  • mount components instead of cy.visit
  • full web application
  • use any Cypress command
  • use any Cypress plugin
  • use network control
  • same debugging experience

X Component Testing

  • any framework
  • fast
  • real browser
  • complement to E2E

Conclusions

  • Cypress is a good choice for Angular projects
  • Try network control, you will like it
  • Use code coverage as a guide to tests to write
  • Component testing is here

Gleb Bahmutov

@bahmutov

Sr Director of Engineering

Mercari US

Thank you 👏