End-vue-End Testing

VueNYC meetup

Sept 24, 2018

Dr Gleb Bahmutov PhD

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

We going to need some tests

E2E

integration

unit

Smallest pieces

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.

Unit tests pass...

E2E

integration

unit

Component

HelloWorld.vue component

Jest test for HelloWorld.vue

@vue/test-utils

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)
})

@vue/test-utils

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

Web application

E2E

integration

unit

Web application

  • Open real browser
  • Load actual app
  • Interact with app like a real user
  • See if it works
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)

  • "cypress.json" - all Cypress settings
  • "cypress/integration" - test files (specs)
  • "cypress/fixtures" - mock data
  • "cypress/plugins" - extending Cypress
  • "cypress/support" - shared commands, utilities

File structure

// 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

Cypress demo

  • Typical test
  • Failing test
  • Recording tests
  • CI setup
  • Network spy / stub
  • Vuex spy / stub
  • Cypress dashboard
  • "cypress open" - GUI interactive mode

  • "cypress run" - headless mode

Cypress CLI has 2 main commands

full video of the run, screenshots of every  failure

(show failing test demo run)

  • Every CI should be good

  • Or use a Docker image we provide

Running E2E on CI

Control network

const fetchTodos = () => cy.request('/todos').its('body')

it('adds todo', () => {
  addTodo('first todo')
  addTodo('second todo')
  fetchTodos().should('have.length', 2)
})

Spy on network calls

it('loads todos on start', () => {
  cy.server()
  cy.route('/todos').as('todos')
  cy.visit('/')
  cy.wait('@todos')
})

Stub server response

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')
})

Control application

// 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)

DOM to Vuex test

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

Vuex to DOM test

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

Other Demos

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

Paid Features 💵

Paid Features 💵: artifacts

test output, video, screenshots

Paid Features 💵: artifacts

FREE for OSS projects

test output, video, screenshots

Paid Features 💵: load balancing

cypress run --record
cypress run --record --group single
cypress run --record --group parallel --parallel

Most CIs should just work 🙏

Paid Features 💵: load balancing

Paid Features 💵

  • Keeps all test data for you

  • Makes use of test history to do more than a single test runner can (load balancing)

FREE for OSS projects

Cypress is out of Beta 🚀

Let's Go Back to Component

Tests

@vue/test-utils

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

Need:

  • real browser (DOM, storage, ...)
  • acting like a real user
  • state cleanup between tests
  • stubbing server

E2E

integration

unit

Need:

WAIT A MINUTE!

  • real browser (DOM, storage, ...)
  • acting like a real user
  • state cleanup between tests
  • stubbing server

E2E

integration

unit

@vue/test-utils

E2E

integration

unit

Cypress

$ 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

Interact, inspect, use

cypress-vue-unit-test: that's how we found

Amir Rustamzadeh

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)
})

Unit test

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)
})

Integration test?

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)
})

Integration test?

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,

...

Unit test

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

🔋🔋🔋🔋🔋🔋🔋🔋🔋🔋🔋

If you can write E2E tests in a framework-agnostic way

You can replace framework X with Vue.js

(same with component tests)

Why Vue developers love Cypress

  • Fast, flake-free

  • GUI, time travel

  • Test recording

  • Documentation

Future work

  • Retries / flake factor

  • Cross-browser

  • Full network stubbing

  • so many more ideas ...

"Cypress vs Nightwatch" is coming to vuejsdevelopers.com ...

Thank you 👏

VueNYC meetup

Sept 24, 2018