Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
📺 Watch this presentation at https://www.youtube.com/watch?v=LEeoQp1j93I
https://lizkeogh.com/2019/07/02/off-the-charts/
+3 degrees Celsius will be the end.
const person = {
greet () {
console.log('Hello')
}
}
// somewhere inside the app
person.greet()
can we confirm the app calls "greet"?
const person = {
greet () {
console.log('Hello')
}
}
// somewhere inside the app
person.greet()
cy.spy(person, 'greet').as('greet')
cy.get('@greet')
.should('have.been.calledOnce')
function spy(o, methodName) {
const method = o[methodName].bind(o)
o[methodName] = function () {
this.called += 1
return method.apply(null, arguments)
}
}
function stub(o, methodName, value) {
o[methodName] = function () {
this.called += 1
return value
}
}
const person = {
greet () {
console.log('Hello')
}
}
// somewhere inside the app
person.greet()
cy.spy(person, 'greet').as('greet')
cy.get('@greet')
.should('have.been.calledOnce')
const person = {
greet () {
console.log('Hello')
}
}
if (window.Cypress) {
window.person = person
}
Maybe expose the reference when running inside Cypress
cy.spy(person, 'greet').as('greet')
"person" is an implementation detail 😐
cy.spy(console, 'log').as('log')
you can spy or stub Browser APIs (window, console, navigator, etc) 🙂
cy.get('@log')
.should('have.been.calledOnceWith', 'Hello')
confirm the application's behavior at the periphery: DOM, console, cookies, network 🥳
<a href="/about.html" target="_blank">About</a>
<script>
document.querySelector('a')
.addEventListener('click', (e) => {
e.preventDefault()
window.open('/about.html')
})
</script>
How to prevent the window.open from opening in a new tab?
it('opens the about page', () => {
cy.visit('index.html')
cy.window().then(win => {
cy.stub(win, 'open').as('open')
})
cy.get('a').click()
cy.get('@open')
.should('have.been.calledOnceWithExactly', '/about.html')
})
it('opens the about page', () => {
cy.visit('index.html')
cy.window().then(win => {
cy.stub(win, 'open').as('open')
})
cy.get('a').click()
cy.get('@open')
.should('have.been.calledOnceWithExactly', '/about.html')
})
it('opens the about page', () => {
cy.visit('index.html')
cy.window().then(win => {
cy.stub(win, 'open').callsFake((url, target) => {
expect(target).to.be.undefined
return win.open.wrappedMethod.call(win, url, '_self')
}).as('open')
})
cy.get('a').click()
cy.get('@open')
.should('have.been.calledOnceWithExactly', '/about.html')
})
it('opens the about page', () => {
cy.visit('index.html')
cy.window().then(win => {
cy.stub(win, 'open').callsFake((url, target) => {
expect(target).to.be.undefined
return win.open.wrappedMethod.call(win, url, '_self')
}).as('open')
})
cy.get('a').click()
cy.get('@open')
.should('have.been.calledOnceWithExactly', '/about.html')
})
const person = {
greet () {
console.log('Hello')
}
}
// somewhere inside the app
person.greet()
cy.spy(person, 'greet').as('greet')
cy.get('@greet')
.should('have.been.calledOnce')
1
2
3
Auto-retry does this
it('opens the about page', () => {
cy.visit('index.html')
cy.window().then(win => {
cy.stub(win, 'open').as('open')
})
cy.get('a').click()
cy.get('@open')
.should('have.been.calledOnceWithExactly', '/about.html')
})
1. set up the stub
2. then click
3. confirm browser behavior
<body>
<script>
window.open('/about.html')
</script>
</body>
What if the app calls window.open immediately?
it('opens the about page', () => {
cy.visit('index.html')
cy.window().then(win => {
cy.stub(win, 'open').as('open')
})
})
too late!
<body>
<script>
window.open('/about.html')
</script>
</body>
What if the app calls window.open immediately?
it('opens the about page', () => {
cy.window().then(win => {
cy.stub(win, 'open').as('open')
})
cy.visit('index.html')
})
⛔️ No, there is no window object yet
<body>
<script>
window.open('/about.html')
</script>
</body>
What if the app calls window.open immediately?
it('opens the about page', () => {
cy.visit('index.html', {
onBeforeLoad (win) {
cy.stub(win, 'open').as('open')
}
}
})
✅ window is there, but before any application code loads
<body>
<script>
window.open('/about.html')
</script>
</body>
What if the app calls window.open immediately?
beforeEach(() => {
cy.on('window:before:load', (win) => {
cy.stub(win, 'open').as('open')
})
})
it('opens the about page', () => {
cy.visit('index.html')
})
✅ Stub window on every page visited
beforeEach(() => {
cy.on('window:before:load', (win) => {
cy.stub(win, 'open').as('open')
})
})
it('opens the about page', () => {
cy.visit('index.html')
})
using cy.on inside the callback
requires test or hook function
beforeEach(() => {
cy.on('window:before:load', (win) => {
cy.stub(win, 'open').as('open')
})
})
it('opens the about page', () => {
cy.visit('index.html')
})
Cypress.on('window:before:load', (win) => {
delete win.fetch
})
it('falls back to XMLHttpRequest', () => {
cy.visit('index.html')
})
using cy.on inside the callback
requires test or hook function
cannot use any cy commands
can be outside any test or hook
cy.window().then(win => {
win.XMLHttpRequest = (options) => {
const ajax = {
open: cy.spy(),
send: cy.spy(),
...
}
...
return ajax
}
})
cy.route is just book-keeping
cy.server and cy.route stub the call to XMLHttpRequest before it leaves the browser
Browser window
App iframe
If application was calling window.fetch
Cypress.on('window:before:load', (win) => {
delete win.fetch
// hope the app polyfills fetch
// via XMLHttpRequest
})
{
"experimentalFetchPolyfill": true
}
we will inject polyfill if necessary
cy.window().then(win => {
cy.stub(win, 'fetch').resolves(...)
})
essentially ...
Intercept HTTP calls here
Any request can be observed or stubbed
(+ ServiceWorker, WebWorker)
cy.server()
cy.route()
Deprecated
Will be removed when we feel cy.intercept() can do it all
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({
pathname: '/search',
query: {
q: 'some terms'
}
})
cy.intercept({
// this RegExp matches any URL beginning with
// 'http://api.example.com/widgets'
url: /^http:\/\/api\.example\.com\/widgets/,
headers: {
'x-requested-with': 'exampleClient'
}
})
// does not reply
cy.intercept('*-fruits').as('fruits')
// replies with a fruit
cy.intercept('favorite-*', ['Lemons 🍋']).as('favorite')
// does not reply
cy.intercept('favorite-fruits').as('favorite-fruits')
cy.visit('/')
cy.wait('@fruits') // first route matches
cy.wait('@favorite') // second route matches
// but the third route never gets the request
// since the second route has replied
cy.contains('li', 'Lemons 🍋').should('be.visible')
app makes
GET /favorite-fruits
Command syntax, examples
cy.route vs cy.intercept (cy.route2)
"Stubbing using cy.intercept" recipe
Cypress cy.intercept Problems
🏅 90 cy.intercept example
tests
cy.route and cy.intercept equivalent tests
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
})
}
})
cy.intercept(matcher, callback)
matcher
request
(via Cypress socket message)
Your callback code executes in the browser right where it was created
1
2
3
4
response
(via Cypress socket message)
stub
modified request
cy.intercept(matcher, callback)
matcher
response
(via Cypress socket message)
You want to access the response with:
req.reply(res => {
...
})
1
2
3
modified response
(via Cypress socket message)
modified response
// server
app.get('/req-headers', (req, res) => {
res.json(req.headers)
})
it('adds request header', () => {
cy.visit('/headers')
cy.intercept('/req-headers', (req) => {
req.headers['x-custom-headers'] = 'added by cy.intercept'
})
cy.get('#get-headers').click()
cy.contains('#output', 'accept-language')
.should('contain', 'x-custom-header')
.and('contain', 'added by cy.intercept')
})
// server
app.get('/req-headers', (req, res) => {
res.json(req.headers)
})
browser does not show the custom header we have added inside cy.intercept
added header was received by the server
Added header is present in the data if you wait on the intercept
cy.intercept('/req-headers', (req) => {
req.headers['x-custom-headers'] = 'added by cy.intercept'
}).as('headers')
cy.get('#get-headers').click()
cy.wait('@headers').its('request.headers')
.should('have.property', 'x-custom-headers', 'added by cy.intercept')
it('modifies the page itself', () => {
// we are only interested in the HTML root resource
cy.intercept({ pathname: '/' }, (req) => {
req.reply((res) => {
res.body += `<footer style="${style}">⚠️ This is a Cypress test ⚠️</footer>`
})
})
cy.visit('/')
cy.contains('footer', 'Cypress test')
.should('be.visible')
})
it('highlights LI elements using injected CSS', () => {
cy.intercept('styles.css', (req) => {
delete req.headers['if-modified-since']
delete req.headers['if-none-match']
req.reply((res) => {
res.send(`${res.body}
li {
border: 1px solid pink;
}
`)
})
})
cy.visit('/')
// confirm the CSS was injected and applied
cy.get('li').should('have.length.gt', 1)
.first().invoke('css', 'border')
.should('be.a', 'string')
.and('include', 'solid')
})
it('highlights LI elements using injected CSS', () => {
cy.intercept('styles.css', (req) => {
delete req.headers['if-modified-since']
delete req.headers['if-none-match']
req.reply((res) => {
res.send(`${res.body}
li {
border: 1px solid pink;
}
`)
})
})
cy.visit('/')
// confirm the CSS was injected and applied
cy.get('li').should('have.length.gt', 1)
.first().invoke('css', 'border')
.should('be.a', 'string')
.and('include', 'solid')
})
Have to think if the browser caches the resource, or if the server responds with "not modified" empty response
it('requests favorite fruits', function () {
cy.intercept('/favorite-fruits').as('fetchFruits')
cy.visit('/fruits.html')
cy.wait('@fetchFruits').its('response.body')
.then((fruits) => {
cy.get('.favorite-fruits li')
.should('have.length', fruits.length)
fruits.forEach((fruit) => {
cy.contains('.favorite-fruits li', fruit)
})
})
})
Data inside the intercept
// wait on the request once
cy.wait('@fetchFruits')
// but get the latest request as many times as needed
cy.get('@fetchFruits').its('response.statusCode')
.should('eq', 200)
cy.get('@fetchFruits').its('response.body')
.should('have.length.gt', 3)
Multiple assertions against intercept
cy.wait + cy.get(s)
cy.wait('@fetchFruits').then(intercept => {
expect(intercept.response.statusCode, 'status code').to.equal(200)
expect(intercept.response.body, 'at least 3 fruits').to.have.length.gt(3)
})
Multiple assertions against intercept
cy.wait + cy.then
Every 30 seconds GET /favorite-fruits
Confirm then GET /favorite-fruits happens once at the start
it('does not fetch for at least five seconds', () => {
let polled
cy.intercept('/favorite-fruits', () => {
polled = true
})
cy.visit('/')
cy.wrap()
.should(() => {
expect(polled, 'fetched fruits').to.be.true
polled = false
})
// physically wait 5 seconds
cy.wait(5000)
.then(() => {
expect(polled, 'no new requests').to.be.false
})
})
it('does not fetch for at least five seconds (cy.spy)', () => {
cy.intercept('/favorite-fruits',
cy.spy().as('reqForFruits') )
cy.visit('/')
// at some point the request happens
cy.get('@reqForFruits').should('have.been.calledOnce')
// physically wait 5 seconds
cy.wait(5000)
// new network calls have not happened
cy.get('@reqForFruits').should('have.been.calledOnce')
})
Using cy.spy() to count calls
if you do not call req.reply() it is a network spy
Confirm then GET /favorite-fruits happens every 30 seconds
without waiting
cy.visit('/')
cy.get('@reqForFruits').should('have.been.calledOnce')
cy.wait(30000)
cy.get('@reqForFruits').should('have.been.calledTwice')
it('fetches every 30 seconds', () => {
cy.clock()
cy.intercept('/favorite-fruits', cy.spy().as('reqForFruits'))
cy.visit('/fruits.html')
// at some point the request happens
cy.get('@reqForFruits').should('have.been.calledOnce')
cy.tick(5000)
// no new network calls
cy.get('@reqForFruits').should('have.been.calledOnce')
// but add 25 more seconds, and the app should have made a network call
cy.tick(25000)
cy.get('@reqForFruits').should('have.been.calledTwice')
})
it('shows loading element', () => {
cy.intercept('/favorite-fruits', {
body: ['Apple', 'Banana', 'Cantaloupe'],
delay: 25000
})
cy.visit('/fruits.html')
})
it('shows loading element', () => {
cy.intercept('/favorite-fruits', {
body: ['Apple', 'Banana', 'Cantaloupe'],
delay: 1000
})
cy.visit('/fruits.html')
cy.get('.loader').should('be.visible')
cy.get('.loader').should('not.exist')
})
Is 1 second enough? Or too much?
it('slows the reply by returning a Promise', () => {
const fruits = ['Apple', 'Banana', 'Cantaloupe']
cy.intercept('/favorite-fruits', (req) =>
Cypress.Promise.delay(1000, fruits).then(req.reply)
)
cy.visit('/fruits.html')
cy.get('.loader').should('be.visible')
cy.get('.loader').should('not.exist')
})
it('shows loading element for as little as possible', () => {
let sendResponse
const p = new Cypress.Promise((resolve) => {
// save the resolve method
// so this promise resolves when we call it
sendResponse = resolve
})
cy.intercept('/favorite-fruits', (req) => {
// wait for the trigger promise to resolve
return p.then(() => req.reply(['Apple', 'Banana', 'Cantaloupe']))
})
cy.visit('/fruits.html')
cy.get('.loader').should('be.visible').then(sendResponse)
cy.get('.loader').should('not.exist')
})
it('stubs the redirect', () => {
cy.intercept('/getout', (req) => {
req.reply((res) => {
expect(res.statusCode).to.equal(302)
// the server wants to redirect us to another domain
expect(res.headers).to.have.property('location', 'https://www.cypress.io')
res.headers.location = '/'
// need to provide something for the updated "res"
// object to be used
// https://github.com/cypress-io/cypress/issues/9555
res.send('stay here')
})
})
cy.get('#getout').click()
cy.location('pathname').should('equal', '/') // redirect worked
})
it('returns different fruits every 30 seconds', () => {
cy.clock()
let k = 0
// return difference responses on each call
cy.intercept('/favorite-fruits', (req) => {
k += 1
switch (k) {
case 1:
return req.reply(['apples 🍎'])
case 2:
return req.reply(['grapes 🍇'])
default:
return req.reply(['kiwi 🥝'])
}
})
cy.visit('/fruits.html')
cy.contains('apples 🍎')
cy.tick(30000)
cy.contains('grapes 🍇')
cy.tick(30000)
cy.contains('kiwi 🥝')
})
// return difference responses on each call
const responses = [
['apples 🍎'], ['grapes 🍇']
]
cy.intercept('/favorite-fruits', (req) => {
req.reply(responses.shift() || ['kiwi 🥝'])
})
You can program your own logic to dynamically vary intercepts
We are still trying to come up with a general way to overwrite interceptors
Issues with label "pkg/net-stubbing"
Plus closing bugs
https://on.cypress.io/intercept
https://glebbahmutov.com/blog/cy-route-vs-route2/
https://github.com/cypress-io/cypress-example-recipes
https://glebbahmutov.com/blog/cypress-intercept-problems/
https://github.com/cypress-io/testing-workshop-cypress
https://slides.com/bahmutov/how-cy-intercept-works
https://www.youtube.com/glebbahmutov
@bahmutov
By Gleb Bahmutov
In this presentation, Gleb Bahmutov explains how the new cy.intercept command works to spy or stub network calls from your web application. He will explain how the intercept works under the hood and how to avoid several common testing problems. Everyone writing Cypress tests would benefit from learning about cy.intercept command and from watching this presentation. Video at https://www.youtube.com/watch?v=LEeoQp1j93I
JavaScript ninja, image processing expert, software quality fanatic