Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
VueNYC meetup
Sept 24, 2018
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional
EveryScape
virtual tours
MathWorks
MatLab on the web
Kensho
finance dashboards
C, C++
Java, C#
CoffeeScript
JavaScript
Angular, Node
Node, Hyperapp, Vue
Cypress.io open source E2E test runner
12 people (Atlanta, LA, Philly, Boston, Chicago)
Quality software behaves the way users expect it to behave
E2E
integration
unit
E2E
integration
unit
Tape, QUnit, Mocha, Ava, Jest
Delightful JavaScript Testing
$ npm install --save-dev jest
$ node_modules/.bin/jest
/* eslint-env jest */
describe('add', () => {
const add = require('.').add
it('adds numbers', () => {
expect(add(1, 2)).toBe(3)
})
})
$ jest
PASS ./test.js
add
✓ adds numbers (4ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.763s, estimated 1s
Ran all test suites.
E2E
integration
unit
HelloWorld.vue component
Jest test for HelloWorld.vue
it('button click should increment the count', () => {
expect(wrapper.vm.count).toBe(0)
const button = wrapper.find('button')
button.trigger('click')
expect(wrapper.vm.count).toBe(1)
})
wrapper.setData({ count: 10 })
wrapper.setProps({ foo: 'bar' })
wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)
+ Mocking, spying, injections, triggering events
import { render } from '@vue/server-test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', () => {
const $route = { path: 'http://www.example-path.com' }
const wrapper = render(Foo, {
mocks: {
$route
}
})
expect(wrapper.text()).toContain($route.path)
})
})
But does it really work?
import { render } from '@vue/server-test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', () => {
const $route = { path: 'http://www.example-path.com' }
const wrapper = render(Foo, {
mocks: {
$route
}
})
expect(wrapper.text()).toContain($route.path)
})
})
or is it working for
test-utils + js-dom?
E2E
integration
unit
E2E
integration
unit
Vue.use(Vuex)
const store = new Vuex.Store({ ... })
// store makes HTTP calls to the server
const app = new Vue({
store,
el: '.todoapp',
created () {
this.$store.dispatch('loadTodos')
},
...
methods: {
setNewTodo (e) {
this.$store.dispatch('setNewTodo', e.target.value)
},
...
})
window.app = app
})
$ npm install -D cypress
(if you have not picked Cypress from vue-cli)
// ui-spec.js
it('loads the app', () => {
cy.visit('http://localhost:3030')
cy.get('.todoapp').should('be.visible')
})
Mocha BDD syntax
Chai assertions
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)
})
fluent API like jQuery
import {resetDatabase} from '../utils'
const visit = () => cy.visit('/')
describe('UI', () => {
beforeEach(resetDatabase)
beforeEach(visit)
context('basic features', () => {
it('starts with zero items', () => {
cy.get('.todo-list')
.find('li')
.should('have.length', 0)
})
})
})
bundling included
base url in cypress.json
cy.request, cy.task, cy.exec
Power tip: add "reference" line to spec JS file to turn on IntelliSense
full video of the run, screenshots of every failure
(show failing test demo run)
const fetchTodos = () => cy.request('/todos').its('body')
it('adds todo', () => {
addTodo('first todo')
addTodo('second todo')
fetchTodos().should('have.length', 2)
})
it('loads todos on start', () => {
cy.server()
cy.route('/todos').as('todos')
cy.visit('/')
cy.wait('@todos')
})
it('loads todos from fixture file', () => {
cy.server()
// loads response from "cypress/fixtures/todos.json"
cy.route('/todos', 'fixture:todos')
cy.visit('/')
getTodoItems()
.should('have.length', 2)
.contains('li', 'mock second')
.find('.toggle')
.should('be.checked')
})
// app.js
function randomId () {
return Math.random().toString().substr(2, 10)
}
// cypress/integration/spec.js
const stubMathRandom = () => {
// first two digits are disregarded,
// so our "random" sequence of ids
// should be '1', '2', '3', ...
let counter = 101
cy.window().then(win => {
cy.stub(win.Math, 'random').callsFake(() => counter++)
})
}
beforeEach(stubMathRandom)
const getStore = () =>
cy.window().its('app.$store')
const getStoreTodos = () =>
getStore().its('todos')
beforeEach(stubMathRandom)
it('can add a particular todo', () => {
const title = `a single todo ${newId()}`
enterTodo(title)
getStoreTodos().should('deep.equal', [{
title,
completed: false,
id: '1'
}])
})
Drive via DOM
Assert Vuex store
const getStore = () =>
cy.window().its('app.$store')
it('changes the ui', () => {
getStore().then(store => {
store.dispatch('setNewTodo', 'a new todo')
store.dispatch('addTodo')
store.dispatch('clearNewTodo')
})
// assert UI
getTodoItems().should('have.length', 1)
.first().contains('a new todo')
})
Drive via Vuex
Assert DOM
many presentations and videos about Cypress
Egghead.io Cypress Course
Free course (same author Andrew Van Slaars!)
Cypress documentation
Cypress examples
Power tip: use doc search
test output, video, screenshots
test output, video, screenshots
cypress run --record
cypress run --record --group single
cypress run --record --group parallel --parallel
Most CIs should just work 🙏
FREE for OSS projects
Vue Test Utils tests Vue components by mounting them in isolation, mocking the necessary inputs (props, injections and user events) and asserting the outputs (render result, emitted custom events).
E2E
integration
unit
E2E
integration
unit
WAIT A MINUTE!
E2E
integration
unit
@vue/test-utils
E2E
integration
unit
$ npm install -D cypress cypress-vue-unit-test
const mountVue = require('cypress-vue-unit-test')
describe('My Vue', () => {
beforeEach(mountVue(/* my Vue code */, /* options */))
it('renders', () => {
// Any Cypress command
// Cypress.vue is the mounted component reference
})
})
Vue component test demo with Cypress
Test components from these frameworks with ease
function add(a, b) {
return a + b
}
add(a, b)
outputs
inputs
a, b
returned value
it('adds', () => {
expect(add(2,3)).to.equal(5)
})
function add(a, b) {
const el = document.getElementById('result')
el.innerText = a + b
}
add(a, b)
outputs
inputs
a, b
DOM
it('adds', () => {
add(2,3)
const el = document.getElementById('result')
expect(el.innerText).to.equal(5)
})
function add() {
const {a, b} =
JSON.parse(localStorage.getItem('inputs'))
const el = document.getElementById('result')
el.innerText = a + b
}
add(a, b)
outputs
inputs
localStorage
DOM
it('adds', () => {
localStorage.setItem('inputs') =
JSON.stringify({a: 2, b: 3})
add()
const el = document.getElementById('result')
expect(el.innerText).to.equal(5)
})
component
outputs
inputs
DOM,
localStorage,
location,
HTTP,
cookies
WW, SW,
...
DOM,
localStorage,
location,
HTTP,
cookies
WW, SW,
...
component
outputs
inputs
DOM,
localStorage,
location,
HTTP,
cookies
WW, SW,
...
DOM,
localStorage,
location,
HTTP,
cookies
WW, SW,
...
Set up
Assert
Mount
E2E
unit
it('logs user in', () => {
cy.visit('page.com')
cy.get('#login').click()
})
E2E
unit
it('logs user in', () => {
mount(LoginComponent)
cy.get('#login').click()
})
E2E
unit
Use same syntax, life cycle and Cypress API
🔋🔋🔋🔋🔋🔋🔋🔋🔋🔋🔋
You can replace framework X with Vue.js
(same with component tests)
VueNYC meetup
Sept 24, 2018
By Gleb Bahmutov
Testing Vue applications can be quick and painless. In this presentation, I will show how to test small parts of Vue apps using vue-test-utils, and how to move "up" the testing pyramid to component and end-to-end tests using Cypress. Presented at VueNYC meetup in Sept 2018. Video at https://www.youtube.com/watch?v=uowaTHQDcKc
JavaScript ninja, image processing expert, software quality fanatic