Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
Video available at https://www.youtube.com/watch?v=JL3QKQO80fs
https://lizkeogh.com/2019/07/02/off-the-charts/
+3 degrees Celsius will be the end.
If there is a company that fights global climate catastrophe and needs JavaScript and testing skills - I will do for free.
Example: https://fab.earth
fight apathy before it kills us all
these slides
Prettier
ESLint
@ts-check
Code is easier to read and understand
Actual linter: catches JS things that can go wrong
Strict(er) linter: catches JS and your type errors
Step 1: code in JS
const add = (a, b) => a + b
add(2, 'foo')
Step 2: add comment
VSCode extension "Document This"
https://marketplace.visualstudio.com/items?itemName=joelday.docthis
Nice: IntelliSense
Step 3: @ts-check
CLI tsc check
$ npx tsc --noEmit --allowJs app.js
app.js:9:8 - error TS2345: Argument of type '"foo"' is not assignable
to parameter of type 'number'.
9 add(2, 'foo')
~~~~~
Found 1 error.
IntelliSense in JS test files when you are using Cypress. Hovering over ".type" command
Documentation page for "cy.type" at https://on.cypress.io/type
I think using it
Static types, linting, JSDoc comments, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples, examples
E2E
integration
unit
(click down arrow to see types of tests)
const add = (a, b) => a + b
it('adds numbers', () => {
expect(add(2, 3)).to.equal(5)
})
import { HelloState } from '../src/hello-x.jsx'
import { HelloState } from '../src/hello-x.jsx'
import React from 'react'
it('changes state', () => {
cy.mount(<HelloState />)
cy.contains('Hello Spider-man!')
const stateToSet = { name: 'React' }
cy.get(HelloState).invoke('setState', stateToSet)
cy.get(HelloState)
.its('state')
.should('deep.equal', stateToSet)
cy.contains('Hello React!')
})
it('yields result that has log messages', () => {
cy.api({ url: '/' }, 'hello world')
.then(({ messages }) => {
const logs = Cypress._.filter(messages, {
type: 'console',
namespace: 'log'
})
expect(logs, '1 console.log message').to.have.length(1)
expect(logs[0]).to.deep.include({
type: 'console',
namespace: 'log',
message: 'processing GET /'
})
})
})
it('adds todos', () => {
cy.visit('/')
cy.get('.new-todo')
.type('write code{enter}')
.type('write tests{enter}')
.type('deploy{enter}')
cy.get('.todo').should('have.length', 3)
})
it('draws pizza correctly', function () {
cy.percySnapshot('Empty Pizza')
cy.enterDeliveryInformation()
const toppings = ['Pepperoni', 'Chili', 'Onion']
cy.pickToppings(...toppings)
// make sure the web app has updated
cy.contains('.pizza-summary__total-price', 'Total: $12.06')
cy.percySnapshot(toppings.join(' - '))
})
<style type="text/css">
.st0{fill:#FFD8A1;}
.st1{fill:#E8C08A;}
- .st2{fill:#FFDC71;}
+ .st2{fill:#71FF71;}
.st3{fill:#DFBA86;}
</style>
changes crust color SVG
it('has good contrast', () => {
cy.visit('/')
cy.injectAxe()
cy.checkA11y({
runOnly: ['cat.color'],
})
})
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
typical Todo application http://todomvc.com/
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
it('adds todos', () => { ... })
it('completes todos', () => {
... })
Ohhh, we don't have a test for Feature C
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
it('adds todos', () => { ... })
it('completes todos', () => {
... })
it('deletes todos', () => {
... })
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
source code
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
source code
it('adds todos', () => { ... })
it('completes todos', () => {
... })
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
source code
it('adds todos', () => { ... })
it('completes todos', () => {
... })
green: lines executed during the tests
red: lines NOT executed during the tests
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
source code
it('adds todos', () => { ... })
it('completes todos', () => {
... })
it('deletes todos', () => {
... })
Are we testing all features?
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
source code
it('adds todos', () => { ... })
it('completes todos', () => {
... })
it('deletes todos', () => {
... })
Code coverage from tests indirectly
measures implemented features tested
Feature A
User can add todo items
Feature B
User can complete todo items
Feature C
User can delete todo items
❚❚❚❚❚
❚❚❚❚❚ ❚❚ ❚❚❚❚
❚❚❚
❚❚❚❚
❚❚❚ ❚❚❚❚❚❚❚
❚❚❚❚
❚❚
❚❚❚❚❚ ❚❚
❚❚❚❚❚❚
❚❚❚
❚❚❚❚❚
❚❚❚❚❚
❚❚❚❚
❚❚❚❚
❚❚❚❚❚❚ ❚❚❚ ❚❚
❚
❚❚
source code
it('adds todos', () => { ... })
it('completes todos', () => {
... })
it('deletes todos', () => {
... })
Unrealistic tests; subset of inputs
code does not implement the feature correctly
it('adds todos', () => {
cy.visit('/')
cy.get('.new-todo')
.type('write code{enter}')
.type('write tests{enter}')
.type('deploy{enter}')
cy.get('.todo').should('have.length', 3)
})
it('adds todos', () => {
cy.visit('/')
cy.get('.new-todo')
.type('write code{enter}')
.type('write tests{enter}')
.type('deploy{enter}')
cy.get('.todo').should('have.length', 3)
})
it('adds todos', () => {
cy.visit('/')
cy.get('.new-todo')
.type('write code{enter}')
.type('write tests{enter}')
.type('deploy{enter}')
cy.get('.todo').should('have.length', 3)
})
We have tested "add todo"
Need tests
Write more end-to-end tests
Ughh, missed it
What is this code doing?
Can I write an E2E test to hit this code?
This code should be unreachable from the user interface
import {getVisibleTodos} from '../../src/selectors'
describe('getVisibleTodos', () => {
it('throws an error for unknown visibility filter', () => {
expect(() => {
getVisibleTodos({
todos: [],
visibilityFilter: 'unknown-filter'
})
}).to.throw()
})
})
@cypress/code-coverage plugin
We got this line from the unit test
@cypress/code-coverage plugin
Nice job, @cypress/code-coverage plugin
combines e2e and unit test coverage automatically
If you add Istanbul (nyc) to your Node.js server code
{
"scripts": {
"start": "node server",
"start:coverage": "nyc --silent node server",
}
}
And add a GET coverage route
if (global.__coverage__) {
// Express / Hapi / HTTP / Next.js available
require('@cypress/code-coverage/middleware/express')(app)
}
it('player X wins', () => {
play(winnerXPath)
cy.contains('h2', 'Player X wins!')
})
it('player O wins', () => {
play(winnerOPath)
cy.contains('h2', 'Player O wins!')
})
draws.forEach((draw, k) => {
it(`plays to a draw ${k}`, () => {
const drawPath = shortestValuePaths[draw]
play(drawPath)
cy.contains('h2', 'Draw')
})
})
Cypress running autogenerated tests
Øredev 2018 https://vimeo.com/311931793
Tests are kept short because they run in the terminal. Their shortness helps to debug a failed test.
Go ahead
class MasterForm extends React.Component {
constructor (props) {
super(props)
if (window.Cypress) {
window.app = this
}
}
...
}
Now tests can control the application directly
expose app reference
cy.contains('Next').click()
cy.log('Second page')
cy.contains('h1', 'Book Hotel 2')
cy.window()
.its('app.state')
.should('deep.equal', startOfSecondPageState)
cy.window()
.its('app')
.invoke('setState', startOfSecondPageState)
cy.log('Second page')
cy.contains('h1', 'Book Hotel 2')
cy.get('#username').type('JoeSmith', typeOptions)
Shoot for the ✨
By Gleb Bahmutov
This talk shows how quick and simple it can be to write end-to-end tests for web applications – if your testing tools are not fighting you all the time. I will go over writing E2E tests using Cypress.io, controlling the network during tests, using visual testing and setting up continuous integration to perform E2E tests on each commit. Video at https://www.youtube.com/watch?v=JL3QKQO80fs
JavaScript ninja, image processing expert, software quality fanatic