Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
// cypress/integration/spec.js
// NOT GOING TO WORK
const fs = require('fs')
fs.readFileSync(...)
⛔️ Cannot simply access the file system
most powerful
// cypress/plugins/index.js
const Knex = require('knex')
const knexConfig = require('../../knexfile')
const { Model } = require('objection')
const knex = Knex(knexConfig.development)
Model.knex(knex)
const Person = require('../../models/Person')
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
on('task', {
resetPeopleTable() {
console.log('reset People table')
return Person.query().truncate()
},
})
}
Plugin file runs in Node
See https://github.com/cypress-io/cypress-realworld-app for good cy.task example
API test
UI test
User flow through the web app
Individual piece of code
Server backend
Visual page appearance
Visual test
https://on.cypress.io/visual-testing
Individual framework component
Component test
https://on.cypress.io/component-testing
Responsive design
E2E tests with different viewports
https://on.cypress.io/viewport
Accessability
Node code
Cypress Node test runner
(stay tuned)
describe('my form', () => {
before(() => {
cy.visit('/users/new')
cy.get('#first').type('johnny')
})
it('has validation attr', () => {
cy.get('#first').should('have.attr', 'data-validation', 'required')
})
it('has active class', () => {
cy.get('#first').should('have.class', 'active')
})
it('has formatted first name', () => {
cy.get('#first').should('have.value', 'Johnny') // capitalized first letter
})
})
describe('my form', () => {
before(() => {
cy.visit('/users/new')
})
it('validates and formats first name', () => {
cy.get('#first')
.type('johnny')
.should('have.attr', 'data-validation', 'required')
.and('have.class', 'active')
.and('have.value', 'Johnny')
})
})
it('test', () => {
let username = undefined
cy.visit('https://app.com')
cy.get('.user-name')
.then(($el) => {
// this line evaluates when .then executes
username = $el.text()
})
if (username) {
cy.contains(username).click()
} else {
// this will always run
// because username will always
// evaluate to undefined
cy.contains('My Profile').click()
}
})
queued to be executed
it('test', () => {
let username = undefined
cy.visit('https://app.com')
cy.get('.user-name')
.then(($el) => {
// this line evaluates when .then executes
username = $el.text()
})
if (username) {
cy.contains(username).click()
} else {
// this will always run
// because username will always
// evaluate to undefined
cy.contains('My Profile').click()
}
})
queued to be executed
it('test', () => {
let username = undefined
cy.visit('https://app.com')
cy.get('.user-name')
.then(($el) => {
// this line evaluates when .then executes
username = $el.text()
})
if (username) {
cy.contains(username).click()
} else {
// this will always run
// because username will always
// evaluate to undefined
cy.contains('My Profile').click()
}
})
never runs
queued to be executed
it('test', () => {
cy.visit('https://app.com')
cy.get('.user-name')
.then(($el) => {
// this line evaluates when .then executes
const username = $el.text()
if (username) {
cy.contains(username).click()
} else {
cy.contains('My Profile').click()
}
})
})
context('Navigation', () => {
it('can navigate around the website', () => {
cy.visit('http://localhost:3000');
cy.get('[data-cy="header-link-about"]').click();
cy.get('main:contains("About")');
cy.get('[data-cy="header-link-users"]').click();
cy.get('main h1:contains("Users")');
});
});
navigation test
Home Page ➡ About Page ➡ Users Page
💡 use cy.pause to step through the commands
Red flags 🚩🚩
The command found the wrong "About" text!
it('can navigate around the website (better)', () => {
cy.visit('http://localhost:3000');
cy.get('[data-cy="header-link-about"]').click();
cy.location('pathname').should('match', /\/about$/);
cy.contains('main h1', 'About').should('be.visible');
});
it('can navigate around the website (better)', () => {
cy.visit('http://localhost:3000');
cy.get('[data-cy="header-link-about"]').click();
cy.location('pathname').should('match', /\/about$/);
cy.contains('main h1', 'About').should('be.visible');
});
it('can navigate around the website (better)', () => {
cy.visit('http://localhost:3000');
cy.get('[data-cy="header-link-about"]').click();
cy.location('pathname').should('match', /\/about$/);
cy.contains('main h1', 'About').should('be.visible');
});
// positive
cy.get('.todo-item')
.should('have.length', 2)
.and('have.class', 'completed')
// negative
cy.contains('first todo').should('not.have.class', 'completed')
cy.get('#loading').should('not.be.visible')
it('hides the loading element', () => {
cy.visit('/')
cy.get('.loading').should('not.be.visible')
})
Red 🚩: negative assertion passes
before the XHR
it('uses negative assertion and passes for the wrong reason', () => {
cy.visit('/?delay=3000')
cy.get('.loading').should('not.be.visible')
})
Negative assertions passes even before the Ajax request starts
it('use positive then negative assertion (flakey)', () => {
cy.visit('/?delay=3000')
// first, make sure the loading indicator shows up
cy.get('.loading').should('be.visible')
// then assert it goes away (negative assertion)
cy.get('.loading').should('not.be.visible')
})
it('slows down the network response (works)', () => {
cy.intercept('/todos', {
body: [],
delayMs: 5000
})
cy.visit('/?delay=3000')
// first, make sure the loading indicator shows up
cy.get('.loading').should('be.visible')
// then assert it goes away (negative assertion)
cy.get('.loading').should('not.be.visible')
})
If you are not reading it, you are making a huge mistake
Documentation makes or breaks projects
Every question here is a failure of the documentation
Every question here is probably a:
💥
"show me Hello, World!"
"show me the changelog diff from version X to Y"
"show me a tutorial"
"show me how to do X"
"how do I solve my issue or bug?"
Then add all your documentation to the site reachable from the index
blog
examples
write docs for these searches!
Create 10x more Cypress examples and recipes
If I see a question on a forum, GitHub, Twitter, etc
How do I toggle a checkbox?
App HTML
Test code
Markdown file
Markdown file
Markdown spec also becomes a static docs page
Every example is scraped into the docs index
Instead, update the documentation, or create an example, or write a blog post.
Then answer with a link
Should eliminate their own jobs by writing more and more documentation until the users find everything themselves
(how to open a good GitHub issue)
(how to open a good GitHub issue)
about 30 hours of emails
By Gleb Bahmutov
In this talk, I will discuss the common mistakes developers make when writing Cypress tests and how to avoid them. We will discuss tests that are too short, tests that hardcode data, tests that race against the application, and other mistakes. I believe this presentation will be useful to anyone writing E2E tests using JavaScript. Presented at TestJSSummit in Jan 2021
JavaScript ninja, image processing expert, software quality fanatic