Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
TDC FUTURE
TECHNOLOGY CREATING TOMORROW
https://lizkeogh.com/2019/07/02/off-the-charts/
+3 degrees Celsius will be the end and we are close
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing
50 people. Atlanta, Philly, Boston, NYC, the world
Fast, easy and reliable testing for anything that runs in a browser
$ npm install -D cypress
// ui-spec.js
it('loads the app', () => {
cy.visit('http://localhost:3000')
cy.get('.todoapp').should('be.visible')
})
Mocha BDD syntax
Chai assertions
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
Cypress tests run in the browser and Cypress can interact with the OS via its plugin file that runs in the Node process
describe('intercept', () => {
it('returns different fruits every 30 seconds', () => {
cy.clock()
// return difference responses on each call
// notice the order of the intercepts
cy.intercept('/favorite-fruits', ['kiwi 🥝']) // 3rd, 4th, etc
cy.intercept('/favorite-fruits', { times: 1 }, ['grapes 🍇']) // 2nd
cy.intercept('/favorite-fruits', { times: 1 }, ['apples 🍎']) // 1st
cy.visit('/fruits.html')
cy.contains('apples 🍎')
cy.tick(30000)
cy.contains('grapes 🍇')
// after using the first two intercepts
// forever reply with "kiwi" stub
cy.tick(30000)
cy.contains('kiwi 🥝')
cy.tick(30000)
cy.contains('kiwi 🥝')
cy.tick(30000)
cy.contains('kiwi 🥝')
})
})
controls app clock and mocks network
describe('intercept', () => {
it('returns different fruits every 30 seconds', () => {
cy.clock()
// return difference responses on each call
// notice the order of the intercepts
cy.intercept('/favorite-fruits', ['kiwi 🥝']) // 3rd, 4th, etc
cy.intercept('/favorite-fruits', { times: 1 }, ['grapes 🍇']) // 2nd
cy.intercept('/favorite-fruits', { times: 1 }, ['apples 🍎']) // 1st
cy.visit('/fruits.html')
cy.contains('apples 🍎')
cy.tick(30000)
cy.contains('grapes 🍇')
// after using the first two intercepts
// forever reply with "kiwi" stub
cy.tick(30000)
cy.contains('kiwi 🥝')
cy.tick(30000)
cy.contains('kiwi 🥝')
cy.tick(30000)
cy.contains('kiwi 🥝')
})
})
controls app clock and mocks network
Dec 2021: https://cypresstips.substack.com/
$ npm i -D ramda
# or
$ yarn add -D ramda
import * as R from 'ramda'
Cypress.R = R
cypress/support/index.js
it('works with Ramda', () => {
cy.wrap(Cypress.R.range(1, 5))
.should('deep.equal', [1, 2, 3, 4])
})
cypress/integration/spec.js
Plugin written by the Cypress team
Plugin is written by a Cypress user outside the core team (most common)
Outside plugin, but reviewed by the Cypress team
Trying to prototype a feature by using a plugin first
<form>
<label for="fname">First name:</label><br />
<input type="text" id="fname" name="fname" /><br />
<label for="lname">Last name:</label><br />
<input type="text" id="lname" name="lname" />
</form>
Select the <input> element by its label's text
const getInputByLabel = (label) => {
return cy
.contains('label', label)
.invoke('attr', 'for')
.then((id) => {
// no need to even return the `cy.get`
// Cypress automatically yields it
cy.get('#' + id)
})
}
const getInputByLabel = (label) => {
return cy
.contains('label', label)
.invoke('attr', 'for')
.then((id) => {
// no need to even return the `cy.get`
// Cypress automatically yields it
cy.get('#' + id)
})
}
getInputByLabel('Last name:').type('Smith')
Cypress.Commands.add('getByLabel', (label) => {
cy.log('**getByLabel**')
cy.contains('label', label)
.invoke('attr', 'for')
.then((id) => {
cy.get('#' + id)
})
})
cy.getByLabel('Last name:').type('Smith')
// load type definitions that come with Cypress module
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
/**
* Finds a form element using the label's text
* @param label string
* @example
* cy.getByLabel('First name:').type('Joe')
*/
getByLabel(label: string):
Chainable<JQuery<HTMLElement>>
}
}
add types in src/index.d.ts
const registerCommand = (name = 'getByLabel') => {
const getByCommand = (label) => {
cy.log(`**${name}**`)
cy.contains('label', label)
.invoke('attr', 'for')
.then((id) => {
cy.get('#' + id)
})
}
Cypress.Commands.add(name, getByCommand)
}
module.exports = {
registerCommand,
}
$ npm i -D cypress-get-by-label
# or
$ yarn add -D cypress-get-by-label
const { registerCommand } = require('cypress-get-by-label')
registerCommand()
cy.getByLabel('First name')
// or we could register under a different name
registerCommand('getFormField')
cy.getFormField('Your age')
cypress/support/index.js
$ npm i -D cypress-get-by-label
# or
$ yarn add -D cypress-get-by-label
cypress-testing-library, cypress-file-upload, cypress-axe, cypress-drag-drop, cypress-xpath, @bahmutov/cy-api, etc
const { registerCommand } = require('cypress-get-by-label')
registerCommand()
cy.getByLabel('First name')
// or we could register under a different name
registerCommand('getFormField')
cy.getFormField('Your age')
cypress/support/index.js
import 'cypress-real-events/support'
it('over the theme switcher', () => {
cy.visit('/')
cy.get('[data-cy=add-todo]').type('text title')
cy.get('#theme-switcher').realHover()
})
import 'cypress-real-events/support'
it('over the theme switcher', () => {
cy.visit('/')
cy.get('[data-cy=add-todo]').type('text title')
cy.get('#theme-switcher').realHover()
})
it('is visible', () => {
cy.get('header').should('be.visible')
cy.get('main').should('be.visible')
cy.get('footer').should('be.visible')
})
it('is visible', () => {
const selectors = ['header', 'main', 'footer']
selectors.forEach(selector => {
cy.get(selector).should('be.visible')
})
})
It would be nice to create a separate test for each selector...
it.each = (items, title, cb) => {
items.forEach(item => {
it(title, function () {
return cb.call(this, item)
})
})
}
const selectors = ['header', 'main', 'footer']
it.each(selectors, 'is visible', (selector) => {
cy.get(selector).should('be.visible')
})
// create a separate test for each selector
const selectors = ['header', 'footer', '.new-todo']
it.each(selectors)('element %s is visible', (selector) => {
cy.visit('/')
cy.get(selector).should('be.visible')
})
// creates tests
// "element header is visible"
// "element footer is visible"
// "element .new-todo is visible"
const data = [
// each entry is an array [selector, assertion]
['header', 'be.visible'],
['footer', 'exist']
['.new-todo', 'not.be.visible']
]
it.each(data)('element %s should %s', (selector, assertion) => {
cy.visit('/')
cy.get(selector).should(assertion)
})
// creates tests
// "element header should be.visible"
// "element footer should exist"
// "element .new-todo should not.be.visible"
// repeat the same test 5 times
it.each(5)('test %K of 5', function (k) {
// note the iteration index k is passed to each test
expect(k).to.be.within(0, 4)
})
Repeat the test N times
const items = [1, 2, 3, 4, 5, 6, ...]
it.each(items, 3)(...)
// tests item 1, 4, 7, ...
Filter, take every Nth item
// split all items among 3 specs
// spec-a.js
it.each(items, 3, 0)(...)
// spec-b.js
it.each(items, 3, 1)(...)
// spec-c.js
it.each(items, 3, 2)(...)
Split into N chunks, test one of them
// pick 2 random items from the array and create 2 tests
it.each(Cypress._.sampleSize(items, 2))(...)
Pick N samples from all items
Re-run the test when the application files change
it('works', () => {
...
})
it('loads', () => {
...
})
it('saves', () => {
...
})
It would be nice to run just the "it loads" test...
const _it = it
const grep = Cypress.env('grep')
it = (title, cb) => {
if (title.includes(grep)) {
_it(title, cb)
} else {
_it.skip(title, cb)
}
}
cypress run --env grep=loads
# or
CYPRESS_grep=loads cypress run
Pass plugin config via Env
require('cypress-grep')() // overwrites "it"
it('works', () => {
...
})
it('loads', {tags: '@smoke'}, () => {
...
})
it('saves', () => {
...
})
$ cypress run --env grep=loads
# run all tests tagged "@smoke"
$ cypress run --env grepTags=@smoke
# run all tests NOT tagged "@smoke"
$ cypress run --env grepTags=-@smoke
# run all tests tagged "@smoke" and "@fast"
$ cypress run --env grepTags=@smoke+@fast
# run the test 5 times
$ cypress run --env grep=loads,burn=5
# omit filtered tests
$ cypress run --env grep=loads,grepOmitFiltered=true
# omit specs without filtered tests
$ cypress run --env grep=loads,grepFilterSpecs=true
# run tests without any tags
$ cypress run --env grepUntagged=true
// run filtered tests 100 times
Cypress.grep('hello world', null, 100)
Grep tests from DevTools
Q: How do I click on the Next button until I get to the last page?
import {recurse} from 'cypress-recurse'
recurse(
() => cy.get('[value=next]'),
($button) => $button.attr('disabled') === 'disabled',
{
log: 'Last page',
delay: 500,
post() {
cy.get('[value=next]').click()
},
},
)
import {recurse} from 'cypress-recurse'
recurse(
() => cy.get('[value=next]'),
($button) => $button.attr('disabled') === 'disabled',
{
log: 'Last page',
delay: 500,
post() {
cy.get('[value=next]').click()
},
},
)
function registerUser(username, password) {
...
}
function loginUser(username, password) {
...
}
it('registers user', () => {
const username = 'Test'
const password = 'MySecreT'
registerUser(username, password)
loginUser(username, password)
cy.location('pathname')
.should('equal', '/rooms')
})
function registerUser(username, password) {
...
}
function loginUser(username, password) {
...
}
it('registers user', () => {
const username = 'Test'
const password = 'MySecreT'
registerUser(username, password)
loginUser(username, password)
cy.location('pathname')
.should('equal', '/rooms')
})
registerUser(username, password)
loginUser(username, password)
cy.dataSession({
name: 'user',
setup() {
registerUser(username, password)
},
validate: true,
})
loginUser(username, password)
Runs the test twice. The same user is re-used
Look up any stored data session from the DevTools console
cy.dataSession({
name: 'user',
setup() {
registerUser(username, password)
},
validate: true,
})
loginUser(username, password)
cy.dataSession({
name: 'user',
init() {
cy.task('findUser', username)
},
setup() {
registerUser(username, password)
},
validate: true,
})
loginUser(username, password)
Find the user and cache in memory
The user already is in the database,
and we just opened Cypress
Re-run Cypress and cached user is used
cy.dataSession({
name: 'user',
init() {
cy.task('findUser', username)
},
setup() {
registerUser(username, password)
},
validate: true,
})
loginUser(username, password)
Can we "cache" the login session?
cy.dataSession({
name: 'user',
init() {
cy.task('findUser', username)
},
setup() {
registerUser(username, password)
},
validate: true,
})
cy.dataSession({
name: 'logged in',
setup() {
loginUser(username, password)
cy.getCookie('connect.sid')
},
validate: true,
recreate(cookie) {
cy.setCookie('connect.sid', cookie.value)
cy.visit('/rooms')
},
dependsOn: ['user'],
})
From 4.5 seconds to 240 ms
TDC FUTURE
TECHNOLOGY CREATING TOMORROW
By Gleb Bahmutov
Do you like writing End-to-End tests using Cypress? What if I told you that you can unlock the full power of Cypress by ... writing JavaScript code to extend the built-in Test Runner features? Be able to add more testing features, write shorter tests, have more power at your disposal? Most people only scratch the surface of the web application tests, but if you learn how to use the existing plugins (and there are lots of them!) and write your own can take your tests to the next level.
JavaScript ninja, image processing expert, software quality fanatic