Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional
JavaScript ninja, image processing expert, software quality fanatic, Microsoft MVP
tip: reload page to get new funny definition of "NPM"
11 people. Atlanta, Philly, Boston, LA
Fast, easy and reliable testing for anything that runs in a browser
want to work with me? Ok, not with me, but at Cypress? https://www.cypress.io/jobs
Will Klein
Quality software behaves the way users expect it to behave
E2E
integration
unit
E2E
integration
unit
(enzyme with full rendering)
E2E
integration
unit
E2E
integration
unit
Really important to users
Really important to developers
function add (a, b) {
return a + b
}
function add (a, b) {
return a + b
}
if (!module.parent) {
console.log(add(2, 3))
}
// node ./add
// 5
function add (a, b) {
return a + b
}
module.exports = add
if (!module.parent) {
console.log(add(2, 3))
}
// node ./add
// 5
test('2 + 3', () => {
const add = require('./add')
expect(add(1, 2)).toBe(3)
})
test.js
$ npm t
> add@1.0.0 test /add
> jest
PASS ./test.js
β 2 + 3 (35ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.96s
Ran all test suites.
$ npm t -- --coverage
> add@1.0.0 test /Users/gleb/git/training/javascript/add
> jest "--coverage"
PASS ./test.js
β 2 + 3 (341ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 75 | 50 | 100 | 75 | |
add.js | 75 | 50 | 100 | 75 | 6 |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.167s
Ran all test suites.
open coverage/lcov-report/index.html
require = require('really-need')
test('2 + 3', () => {
const add = require('./add', {
parent: undefined
})
expect(add(1, 2)).toBe(3)
})
Trick a module to think it has no parent
require = require('really-need')
test('2 + 3', () => {
const add = require('./add', {
parent: undefined
})
expect(add(1, 2)).toBe(3)
})
Trick a module to think it has no parent
require = require('really-need')
test('2 + 3', () => {
const add = require('./add', {
parent: undefined
})
expect(add(1, 2)).toBe(3)
})
Trick a module to think it has no parent
works in Node < v4.2.2 π
const exec = require('execa-wrap')
test('2 + 3', () => {
return exec('node', ['./add'])
.then(result => {
expect(result).toContain('5')
})
})
command: node ./add
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
5
-------
stderr:
-------
-------
β
E2E
integration
unit
E2E
unit
expect(add(1, 2)).toBe(3)
exec('node', ['./add'])
.then(result => {
expect(result).toContain('5')
})
E2E
integration
unit
E2E
unit
expect(add(1, 2)).toBe(3)
exec('node', ['./add'])
.then(result => {
expect(result).toContain('5')
})
developer view
user view
$ npm t -- --coverage
> add@1.0.0 test /Users/gleb/git/training/javascript/add
> jest "--coverage"
PASS ./test.js
β 2 + 3 (77ms)
5
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 0 | 0 | 0 | 0 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.323s
Ran all test suites.
π
// change test to
return exec('nyc',
['--reporter=lcov', 'node', './add'])
// npm i -D nyc
test.js
Coverage is hard
Code coverage
is tricky
const isEmail = (s) =>
/^\w+@\w+\.\w{3,4}$/.test(s)
// 1 test = 100% code coverage
β(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
mocha -r data-cover spec.js
Direct your code coverage effort by complexity
$ npm install -D cypress
it('opens the page', () => {
cy.visit('http://localhost:3000')
cy.get('.new-todo')
.should('be.visible')
})
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)
})
Egghead.io Cypress Course
Free course (same Egghead.io author!)
Cypress documentation
image source: http://ktla.com/2016/04/02/small-plane-crashes-into-suv-on-15-freeway-in-san-diego-county/
undefined is not a function
if (process.env.NODE_ENV === 'production') {
var raven = require('raven');
var SENTRY_DSN = 'https://<DSN>@app.getsentry.com/...';
var client = new raven.Client(SENTRY_DSN);
client.patchGlobal();
}
foo.bar // this Error will be reported
npm install raven --save
Defensive coding: checking inputs before each computation (preconditions). Sometimes checking result value before returning (postconditions).
Paranoid coding: checking inputs before each computation, as if the caller was evil and trying to break the function on purpose.
const la = require('lazy-ass')
const is = require('check-more-types')
la(is.strings(names), 'expected list of names', names)
la(is.email(loginName), 'missing email')
la(is.version(tag) || check.sha(tag), 'invalid tag', tag)
unit / E2E tests for happy path
error handling
tests based on crashes
50%
20%
30%
User stories?!
Try to connect separate pieces of software together. ...
This is also why I don't use type system like Typescript - it does not let me hack things together!
function getAddress() {
// "address" is nested inside the result object
return db.fetch(...)
.then(R.path(['customer', 'address']))
.then(Maybe.fromNullable)
.then(R.map(objectToCamelCase))
}
{
"title": "Todo",
"type": "object",
"properties": {
text: {
type: 'string',
description: 'Todo text, like "clean room"',
},
done: {
type: 'boolean',
description: 'Is this todo item completed?',
}
},
"required": ["text", "done"]
}
json-schema
const Todo100: ObjectSchema = {
version: '1.0.0',
schema: {
title: 'Todo', type: 'object',
description: 'Todo item sent by the client',
properties: {
text: {
type: 'string',
description: 'Todo text, like "clean room"',
},
done: {
type: 'boolean',
description: 'Is this todo item completed?',
},
},
required: true, additionalProperties: false,
},
example: {
text: 'do something',
done: false
}
}
versioned schema
const {assertSchema} = require('@cypress/schema-tools')
const assertTodoRequest =
assertSchema(schemas)('Todo', '1.0.0')
assertTodoRequest({
done: true
})
Schema Todo@1.0.0 violated
Errors:
data.text is required
Current object:
{
done: true
}
Expected object like this:
{
done: false,
text: "do something"
}
really verbose error message
Please, invest time in error messages!
api.assertSchema('Todo', '1.0.0')(response.body)
expect(
api.sanitize('Todo', '1.0.0)(response.body)
).toMatchSnapshot()
Sanitize dynamic data based on schema
Follow https://www.cypress.io/blogΒ for blog posts
@bahmutov
cypress.io
By Gleb Bahmutov
In this presentation I will show all the tools I use to write software that does not break but keeps the users happy. Static types, exception monitoring, unit tests with snapshots, randomized testing, code and data coverage, code complexity metrics and awesome end to end testing tools - the list is long and keeps on growing! The techniques I plan to show are applicable to every framework and environment. Presented at BuzzJS 2018, video at https://youtu.be/1PMxLTfh6lo?t=1
JavaScript ninja, image processing expert, software quality fanatic