Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
Sli.do event code #cy-for-ng
https://lizkeogh.com/2019/07/02/off-the-charts/
+3 degrees Celsius will be the end.
Sli.do event code #cy-for-ng
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?
🕵🏻♂️ 🎁
$ 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
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
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
Is there an automated way to convert Protractor tests to Cypress specs?
No, sorry.
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
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
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
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;
}
}
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);
});
});
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 })
})
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 })
})
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)
})
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
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
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('POST', '/graphql', (req) => {
if (req.body.hasOwnProperty('mutation')) {
req.alias = 'gqlMutation'
}
})
// assert that a matching request has been made
cy.wait('@gqlMutation')
*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*
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
})
}
})
// requests to '/users.json' will be fulfilled
// with the contents of the "users.json" fixture
cy.intercept('/users.json',
{ fixture: 'users.json' })
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])
})
})
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])
...
})
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!');
});
});
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!');
});
});
By Gleb Bahmutov
This presentation discusses how Angular developers can take advantage of Cypress Test Runner to write simple and powerful end-to-end tests. We will look at the custom commands, page objects, using fixtures and mocks, dynamic waits, and migrating from Protractor to Cypress.
JavaScript ninja, image processing expert, software quality fanatic