Testing Angular Apps with Cypress
Cecelia Martinez
Technical Account Manager @ Cypress
- Joined Cypress in January 2020
- Women Who Code Front End Volunteer
- Out in Tech Atlanta Founding Member
- Angular Experience Podcast panelist
- Georgia Tech Full Stack Boot Camp grad
- MBA in Marketing, BA in Communications
- Proud dog mom and Atlanta transplant
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
- ✅ 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
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
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
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
Testing Angular Apps with Cypress - WWC San Diego
By Cecelia Martinez
Testing Angular Apps with Cypress - WWC San Diego
Learn about testing your Angular apps with the free, open-source Cypress framework. This talk will cover some of the considerations when migrating from Protractor to Cypress, testing NgRx, and things to keep in mind for testing Angular specifically.
- 2,432