Testing Angular Apps with Cypress

Cecelia Martinez

Technical Account Manager @ Cypress

Agenda

  • Why Cypress?
  • Cypress Angular Schematic
  • Protractor => Cypress
  • Testing NgRx
  • Q&A

Why Cypress?

Why Cypress? 

  • Interact with tests in a browser
  • Faster feedback loops
  • Time travel through tests in interactive mode
  • Screenshots and videos for faster debugging in CI
  • Test retries to identify and mitigate test flake
  • Network spying and stubbing

Cypress Angular Schematic

Cypress Angular Schematic

www.npmjs.com/package/@cypress/schematic

  • ✅ Install Cypress
  • ✅ Add npm scripts for running Cypress in run and open mode
  • ✅ Scaffold base Cypress files and directories
  • ✅ Provide ability to add new e2e files using ng-generate
  • ✅ Optional: prompt to add or update the default ng e2e command to use Cypress

Cypress Angular Schematic

www.npmjs.com/package/@cypress/schematic

Install

ng add @cypress/schematic

Run in open mode

ng run {project-name}:cypress-open

Run headlessly

ng run {project-name}:cypress-run

Cypress Angular Schematic

www.npmjs.com/package/@cypress/schematic

Generate new e2e spec files

ng generate @cypress/schematic:e2e

Add or update to use to run Cypress in open mode

ng e2e

Cypress Angular Schematic

www.npmjs.com/package/@cypress/schematic

Configuration Options:

  • Running the builder with a specific browser
  • Recording test results to the Cypress Dashboard
  • Specifying a custom cypress.json config file
  • Running Cypress in parallel mode within CI

Cypress Angular Schematic

"cypress-open": {
  "builder": "@cypress/schematic:cypress",
  "options": {
    "watch": true,
    "headless": false,
    "browser": "chrome"
  },
},
"cypress-run": {
  "builder": "@cypress/schematic:cypress",
  "options": {
    "devServerTarget": "{project-name}:serve",
    "configFile": "cypress.production.json",
    "parallel": true,
    "record": true,
    "key": "your-cypress-dashboard-recording-key"
  },
}

Protractor => Cypress

Differences in Approach

  • Navigating websites
  • Automatic waits and retries
  • Control flow for asynchronous code
  • Network handling
  • Page Objects

Navigating Websites

Tip: No need to disable waiting for Angular to be enabled when visiting non-Angular pages

it('visits a page', () => {
  browser.get('/about')
  browser.navigate().forward()
  browser.navigate().back()
})

Before: Protractor

it('visits a page', () => {
  cy.visit('/about')
  cy.go('forward')
  cy.go('back')
})

After: Cypress

Automatic Waits/Retries

Tip: Cypress enables you to write tests without the need for waiting, so tests are more predictable

element(by.css('button')).click()
browser.waitForAngular()
expect(by.css('.list-item').getText()).toEqual('my text')

Before: Protractor

cy.get('button').click()
cy.get('.list-item').contains('my text')

After: Cypress

Control Flow for Async Code

  • Cypress and Protractor have similar approaches to async
  • Cypress commands are not invoked immediately, are enqueued to run serially at a later time
  • Cypress API is not an exact implementation of Promises
  • Protractor's WebDriverJS API is based on promises, which is managed by a control flow

Network Handling

  • Protractor has no built-in solution for network spying
  • Cypress can leverage the intercept API to spy on and manage any network request
  • Intercept handles all network requests going in and out of the browser
  • Spy on requests to make assertions
  • Wait for requests to complete before proceeding with test
  • Stub or modify outgoing requests and incoming responses as needed

Page Objects

Yes, you CAN use Page Objects with Cypress. You just may not need them!

// Protractor Page Object

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')
})

Page Objects

Yes, you CAN use Page Objects with Cypress. You just may not need them!

// Cypress Page Object

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 Custom Commands

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

Cypress also provides a Custom Command API to enable you to add methods to use globally.

Cypress Custom Commands

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

Cypress also provides a Custom Command API to enable you to add methods to use globally.

You can also just use regular JavaScript functions!

Differences in Test Syntax

  • Selecting Elements
  • Interacting with Elements
  • Making Assertions

Selecting Elements

element(by.tagName('h1'))
element(by.css('.my-class'))
element(by.id('my-id'))
element(by.cssContainingText('.my-class', 'text'))
element.all(by.tagName('li'))

Before: Protractor

cy.get('h1')
cy.get('.my-class')
cy.get('#my-id')
cy.get('.my-class').contains('text')
cy.get('li')

After: Cypress

Interacting with Elements

element(by.css('button')).click()
element(by.css('input')).sendKeys('my text')
element.all(by.css('[type="checkbox"]')).first().click()

Before: Protractor

cy.get('button').click()
cy.get('input').type('my text')
cy.get('[type="checkbox"]').first().check()

After: Cypress

Making Assertions

const list = element.all(by.css('li.selected'))
expect(list.count()).toBe(3)

Before: Protractor

cy.get('li.selected').should('have.length', 3)

After: Cypress

Expect vs. Should: Length

Making Assertions

expect(element(by.id('user-name')).getText()).toBe('Joe Smith')

Before: Protractor

cy.get('#user-name').should('have.text', 'Joe Smith')

After: Cypress

Expect vs. Should: Text Content

Making Assertions

expect(element(by.tagName('button')).isDisplayed()).toBe(true)

Before: Protractor

cy.get('button').should('be.visible')

After: Cypress

Expect vs. Should: Visibility

Making Assertions

  • Cypress lets you use all Chai assertion styles, including Should, Expect, and Assert
it("gets a list of bank accounts for user", function () {
  const { id: userId } = ctx.authenticatedUser!;
  cy.request("GET", `${apiBankAccounts}`).then((response) => {
    expect(response.status).to.eq(200);
    expect(response.body.results[0].userId).to.eq(userId);
    });
  });
});

Testing NgRx

Testing Approach

  • Expose NgRx store to Cypress
  • Assert on actions
  • Assert on effects
  • Dispatch actions

Expose NgRx store

// Application source code app.component.ts
export class AppComponent {

  constructor(private store: Store<any>) {
    // @ts-ignore
    if(window.Cypress){
      // @ts-ignore
      window.store = this.store;
  }
}

...
  }
}

Asserting on Actions

// Application source code actions.ts
export const getAllSuccess = createAction(
  '[Shows API] Get all shows success',
  props<{ shows }>()
);

You can assert on the type of action last dispatched in the NgRx store, as well as the value of props.

// test code
it("validates getAllSuccess action", () => {

 // code to trigger action here via UI

  cy.window().then(w => {

// tap into the store to access the last action and its value
    const store = w.store;
    const action = store.actionsObserver._value;
    const shows = store.actionsObserver._value.shows;
            
 // expect action type and length of shows array to match expected values
    expect(action.type).equal("[Shows API] Get all shows success");
    expect(shows.length).equal(4);
  });

Asserting on Actions

// application source code effects.ts
favoriteShow$ = createEffect(() =>
  this.actions$.pipe(
    ofType(allShowsActions.favoriteShowClicked, favoriteShowsActions.favoriteShowClicked),
    mergeMap(({ showId }) =>
      this.showsService
        .favoriteShow(showId)
        .pipe(map(() => favoriteShowSuccess({ showId })), catchError(error => of(null)))
...
  );

Asserting on Effects

Dispatching favoriteShowClicked causes the favoriteShowSuccess action with the correct showId

// code to trigger the favoriteShowClicked with id 2 action here

cy.window().then(w => {

  // gets most recent action from store
  const store = w.store;
  const action = store.actionsObserver._value
            
  // confirms it is the effect expected after favoriteShowClicked
  expect(action.type).equal("[Shows API] favorite show success");
  expect(action.showId).equal(2)
});

Asserting on Effects

Dispatching favoriteShowClicked causes the favoriteShowSuccess action with the correct showId

cy.window()
.its('store')
.invoke('dispatch', { showId: 1, type: '[All Shows] favorite show'});

Dispatching Actions

  • You can tap into the store to dispatch actions and bypass the UI to set up test state
  • Use the invoke command and pass 'dispatch' and an object with any required props and the type

Resources

Q&A