Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
LambdaTest
Voices of Community
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
$ 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
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
How do I create a testing framework using Cypress?
You don't have to
Study (?)
$ npm i -D cypress cypress-map prettier
How I start every project...
Cypress v12
adds retriable chains of query commands.
"kiwi" is the last item 🧑🏻
get elements
take the last element
its text should be "kiwi"
🤖
it('shows the kiwi last', () => {
cy.visit('cypress/index.html')
cy.get('#items li').last().should('have.text', 'kiwi')
})
get elements
take the last element
its text should be "kiwi"
🤖
it('shows the kiwi last', () => {
cy.visit('cypress/index.html')
cy.get('#items li').last().should('have.text', 'kiwi')
})
Cypress v11
it('shows the kiwi last', () => {
cy.visit('cypress/index.html')
cy.get('#items li').last().should('have.text', 'kiwi')
})
Cypress v12
it('shows the kiwi last', () => {
cy.visit('cypress/index.html')
cy.get('#items li')
.last()
.invoke('text')
.should('equal', 'kiwi')
})
Cypress v12
it('shows 30 last', () => {
cy.visit('cypress/index.html')
cy.get('#items li')
.last()
.invoke('text')
.then(parseInt)
.should('equal', 30)
})
Cypress v12
no retries
import 'cypress-map'
it('shows 30 last', () => {
cy.visit('cypress/index.html')
cy.get('#items li')
.last()
.invoke('text')
.apply(parseInt) // query from cypress-map
.should('equal', 30)
})
Cypress v12
import 'cypress-map'
it('shows 30 last', () => {
cy.visit('cypress/index.html')
cy.get('#items li')
.last()
.invoke('text')
.apply(parseInt) // query from cypress-map
.should('equal', 30)
})
Cypress v12
import 'cypress-map'
it('shows numbers', () => {
cy.visit('cypress/index.html')
cy.get('#items li')
.map('innerText')
.map(Number)
.should('deep.equal', [10, 20, 30])
})
Cypress v12
import 'cypress-map'
it('shows numbers', () => {
cy.visit('cypress/index.html')
cy.get('#items li')
.map('innerText')
.map(Number)
.should('deep.equal', [10, 20, 30])
})
Cypress v12
cy.table for checking the full table (or part of it)
Cypress tests run in the browser and Cypress can interact with the OS via its plugin file that runs in the Node process
plugins
plugins
Node.js
Process
Browser
Cypress architecture
cypress-map
cypress-each
cypress-recurse
cypress-esbuild-preprocessor
cypress-grep
cypress-watch-and-reload
cypress-data-session
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
(component testing)
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()
})
Cypress is just JavaScript. Anything not bolted down can be overwritten and changed.
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
Unfortunately, Cypress team does not plan to help plugins implement UI elements 😢
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
Subscribe: https://cypresstips.substack.com
100 hands-on lessons, 22 plugins, lots of videos
LambdaTest
Voices of Community
By Gleb Bahmutov
You can supercharge your end-to-end, component, and API Cypress tests using plugins. In this talk, I will show my favorite Cypress plugins, explain how they work, and how easy it is to write simple, elegant testing code. From accessing databases, receiving emails, and setting up data, to repeating commands and checking the page's accessibility - there is a Cypress plugin for everything!
JavaScript ninja, image processing expert, software quality fanatic