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