ConFoo.CA
these slides
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional / testing
18 people. Atlanta, Philly, Boston, Chicago, NYC
Fast, easy and reliable testing for anything that runs in a browser
Don't trust this guy
👉
Defensive coding: checking inputs before each computation (preconditions). Sometimes checking result value before returning (postconditions).
function login(username, password) {
if (username === undefined || password === undefined) {
console.error('you forgot login or password')
// try to handle this somehow
return
}
// login ...
}
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)
// 🚨
assert(condition, 'invalid tag ' + JSON.stringify(tag) +
' among ' + JSON.stringify(tags)
// ✅
la(condition, 'invalid tag', tag, 'among', tags)
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)
predicate
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)
explanation
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)
explanation
undefined is not a function
vs
What went wrong?
Where?
Why?
How do I fix this?
Cypress v3
$ npm install -D cypress
$ npx cypress open
$ npx cypress run --record --parallel
Cypress v3.1.0
Spin N CI machines and
a single CI machine
multiple CI machines
API
Cypress 1.0.0
Cypress 1.1.0
Cypress 2.1.0
...
Dashboard web app
Cypress v3 test runner sending data to API
API
Cypress 1.0.0
Cypress 1.1.0
Cypress 2.1.0
Cypress 3.0.0
...
Dashboard web app
Completely different sequence of calls from the test runner Cypress v3
API
Cypress 1.0.0
Cypress 1.1.0
Cypress 2.1.0
Cypress 3.0.0
...
Dashboard web app
database
old and new
data
Mixture of old and new data in DB
API
Cypress 1.0.0
Cypress 1.1.0
Cypress 2.1.0
Cypress 3.0.0
...
Dashboard web app
database
old and new
data
What data is crossing these borders?
tip: manually updating wiki examples does not ⚖️
{
"title": "PostTodoRequest",
"type": "object",
"properties": {
"text": {
"type": "string"
},
"done": {
"type": "boolean"
}
},
"required": [
"text",
"done"
],
"additionalProperties": false
}
{
"title": "PostTodoRequest",
"type": "object",
"description": "Todo item sent by the client",
"properties": {
"text": {
"type": "string",
"description": "Todo text"
},
"done": {
"type": "boolean",
"description": "Is this todo item completed?"
}
},
"required": ["text", "done"],
"additionalProperties": false
}
{
"version": {"major": 1, "minor": 0, "patch": 0},
"schema": {
"title": "PostTodoRequest",
"type": "object",
"description": "Todo item sent by the client",
"properties": {
"text": {
"type": "string",
"description": "Todo text"
},
"done": {
"type": "boolean",
"description": "Is this todo item completed?"
}
},
"required": ["text", "done"],
"additionalProperties": false
}
}
{
"version": {"major": 1, "minor": 0, "patch": 0},
"schema": {
"title": "PostTodoRequest",
"type": "object",
"description": "Todo item sent by the client",
"properties": {
"text": {
"type": "string",
"description": "Todo text"
},
"done": {
"type": "boolean",
"description": "Is this todo item completed?"
}
},
"required": ["text", "done"],
"additionalProperties": false
},
"example": {"text": "write test", "done": false}
}
import { assertSchema } from '@cypress/schema-tools'
import { schemas } from '../schemas'
const assertTodoRequest = assertSchema(schemas)
('postTodoRequest', '1.0.0')
const todo = {
text: 'use schemas',
done: true,
}
assertTodoRequest(todo)
// all good
valid object
import { assertSchema } from '@cypress/schema-tools'
import { schemas } from '../schemas'
const assertTodoRequest = assertSchema(schemas)
('postTodoRequest', '1.0.0')
const todo = {
done: true,
}
assertTodoRequest(todo)
// Error
bad object
import { assertSchema } from '@cypress/schema-tools'
import { schemas } from '../schemas'
const assertTodoRequest = assertSchema(schemas)
('postTodoRequest', '1.0.0')
const todo = {
done: true,
}
assertTodoRequest(todo)
// Error
bad object
// Explanation
Schema postTodoRequest@1.0.0 violated
Errors:
data.text is required
Current object:
{
"done": true
}
Expected object like this:
{
"done": false,
"text": "do something"
}
message in the thrown error
Schema postTodoRequest@1.0.0 violated
Errors:
data.text is required
Current object:
{
"done": true
}
Expected object like this:
{
"done": false,
"text": "do something"
}
message in the thrown error
What went wrong?
Schema postTodoRequest@1.0.0 violated
Errors:
data.text is required
Current object:
{
"done": true
}
Expected object like this:
{
"done": false,
"text": "do something"
}
message in the thrown error
Why?
Schema postTodoRequest@1.0.0 violated
Errors:
data.text is required
Current object:
{
"done": true
}
Expected object like this:
{
"done": false,
"text": "do something"
}
message in the thrown error
How do I fix this?
Schema postTodoRequest@1.0.0 violated
Errors:
data.text is required
Current object:
{
"done": true
}
Expected object like this:
{
"done": false,
"text": "do something"
}
message in the thrown error
This for real world objects is 100% 💣
Build complex schemas from simpler schemas
complex results are in private cypress-io/json-schemas
const person110 = addProperty(
{
schema: person100,
description: 'Person with title',
},
{
property: 'title',
propertyType: 'string',
propertyFormat: null,
exampleValue: 'mr',
isRequired: false,
propertyDescription: 'How to address this person',
},
)
Tip: run old tests against the new schema
no more manual Wiki edits
some data is hard to deal with
const data = {
"done": false,
"id": 2,
"text": "do something",
"uuid": "3372137d-b582-4e32-807d-af3021112efa"
}
assertTodo(data) // ok
expect(data).toMatchSnapshot() // nope, dynamic value
const data = {
"done": false,
"id": 2,
"text": "do something",
"uuid": "3372137d-b582-4e32-807d-af3021112efa"
}
assertTodo(data) // ok
expect(data).toMatchSnapshot({
uuid: expect.any(String)
}) // ok
const data = {
"done": false,
"id": 2,
"text": "do something",
"uuid": "3372137d-b582-4e32-807d-af3021112efa"
}
assertTodo(data) // ok
expect(data).toMatchSnapshot({
uuid: expect.any(String)
}) // ok
This information is already part of the schema!!!
import { CustomFormat, CustomFormats }
from '@cypress/schema-tools'
const uuid: CustomFormat = {
name: 'uuid', // the name
description: 'GUID used through the system',
detect: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
// (optional) replace actual value with this default value
// when using to sanitize an object
defaultValue: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
}
schema: {
type: 'object',
title: 'Employee',
properties: {
id: {
type: 'string',
format: 'uuid',
},
},
},
example: {
id: 'a368dbfd-08e4-4698-b9a3-b2b660a11835',
}
const data = {
"done": false,
"id": 2,
"text": "do something",
"uuid": "3372137d-b582-4e32-807d-af3021112efa"
}
expect(
sanitizeTodo(assertTodo(data))
).toMatchSnapshot()
exports = {
"done": false,
"id": 2,
"text": "do something",
"uuid": "ffffffff-ffff-ffff-ffff-ffffffffffff"
}
saved snapshot
getBuilds(@user, { page: 2 })
.expect(200)
.expectResSchema("getRunResponse@1.0.0", {
snapshot: 'second page of builds'
})
an object with 30 properties, uuids and timestamps sanitized
r.post "/builds",
schemaVersion.set({
2: {
req: "postRunRequest@1.0.0",
res: "postRunResponse@1.0.0"
}
}),
Builds.Create
Cypress REST API route
local, testing, staging environments: schema violations are thrown errors
production: schema violations are reported to Sentry
r.get "/projects/:project_slug/runs/:build_number",
loggedIn,
schemaVersion.set({
2: {
res: "getRunResponse@2.0.0"
},
3: {
res: "getRunResponse@2.1.0"
}
}),
Builds.GetRun
Versioned responses
Response is trimmed to pass version X.Y.Z to avoid breaking clients
import { trim } from '@cypress/schema-tools'
const trimPerson = trim(schemas, 'Person', '1.0.0')
const person = ... // some result with lots of properties
const trimmed = trimPerson(person)
// trimmed should be valid Person 1.0.0 object
// if the values are actually matching Person@1.0.0
// all extra properties should have been removed
Be liberal in what your inputs. Be conservative in your outpu.
Declarative, Code-First GraphQL Schemas for JavaScript/TypeScript
Tim Griesser, Software Developer at Cypress.io
it('has todo fixture matching schema', () => {
// is our fixture file correct?
cy.fixture('todo')
.then(api.assertSchema('PostTodoRequest', '1.0.0'))
})
it('returns new item matching schema', () => {
cy.server()
cy.route('POST', '/todos').as('post')
cy.visit('/')
cy.get('.new-todo').type('Use schemas{enter}')
cy.wait('@post')
.its('response.body')
.then(api.assertSchema('PostTodoResponse', '1.0.0'))
})
Cypress REST API v2 ➡ v3 went without errors. And we still support v2 client
ConFoo.CA