Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Hi, I’m Roman! @codebryo That’s where I work

Slide 3

Slide 3 text

Jumped on Vue with 0.11

Slide 4

Slide 4 text

revue vue-test-utils ? jest

Slide 5

Slide 5 text

Snapshots Run your code Snapshot the piece You care for. Store it

Slide 6

Slide 6 text

Snapshots const User = { name: 'Tony Tinkerton', job: 'Inventor', age: 42 } test('User snapshot', () !=> { expect(User).toMatchSnapshot() }) // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`User snapshot 1`] = ` Object { "age": 42, "job": "Inventor", "name": "Tony Tinkerton", } `;

Slide 7

Slide 7 text

Randomnessssss

Slide 8

Slide 8 text

Randomnessssss

Slide 9

Slide 9 text

Faker https://github.com/Marak/Faker.js

Slide 10

Slide 10 text

import faker from 'faker' const RandomUser = { name: faker.name.findName(), job: faker.name.jobTitle(), age: faker.random.number({ min: 60}) } Property Matchers + Snapshots

Slide 11

Slide 11 text

import faker from 'faker' const RandomUser = { name: faker.name.findName(), job: faker.name.jobTitle(), age: faker.random.number({ min: 60}) } test('RandomUser snapshot', () !=> { expect(RandomUser).toMatchSnapshot({ name: expect.any(String), job: expect.any(String), age: expect.any(Number) }) })

Slide 12

Slide 12 text

exports[`RandomUser snapshot 1`] = ` Object { "age": Any, "job": Any, "name": Any, } `;

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

export default { state: { !// plain Object }, mutations: { !// simple functions }, actions: { !// special functions that invoke other functions }, getters: { !// computed properties } }

Slide 15

Slide 15 text

export default { state: { fridge: [], }, mutations: { addToFridge(state, ingredient) { state.fridge.push(ingredient) } }, actions: { }, getters: { } }

Slide 16

Slide 16 text

import store from './store' describe('state', () !=> { test('matches default structure', () !=> { expect(store.state).toMatchSnapshot({ fridge: expect.any(Array) }) }) }) exports[`state matches default structure 1`] = ` Object { "fridge": Any, } `;

Slide 17

Slide 17 text

describe('mutations', () !=> { const state = store.state const mutations = store.mutations test('#addToFridge stores ingredients in the fridge', () !=> { mutations.addToFridge(state, 'ice cream') expect(state.fridge).toEqual(['ice cream']) }) })

Slide 18

Slide 18 text

describe('mutations', () !=> { const state = store.state const mutations = store.mutations test('#addToFridge stores ingredients in the fridge', () !=> { mutations.addToFridge(state, 'ice cream') expect(state.fridge).toEqual(['ice cream']) }) })

Slide 19

Slide 19 text

[‘ice cream’, 2] describe('mutations', () !=> { const state = store.state const mutations = store.mutations test('#addToFridge stores ingredients in the fridge', () !=> { mutations.addToFridge(state, 'ice cream') expect(state.fridge).toEqual(['ice cream']) }) test('#addToFridge also takes numbers', () !=> { mutations.addToFridge(state, 2) expect(state.fridge).toEqual([2]) }) })

Slide 20

Slide 20 text

describe('mutations', () !=> { let state const mutations = store.mutations beforeEach(() !=> { state = Object.assign({}, store.state) !// or Clone or something }) test('#addToFridge stores ingredients in the fridge', () !=> { mutations.addToFridge(state, 'ice cream') expect(state.fridge).toEqual(['ice cream']) }) test('#addToFridge also takes numbers', () !=> { mutations.addToFridge(state, 2) expect(state.fridge).toEqual([2]) }) }) let state const mutations = store.mutations beforeEach(() !=> { state = Object.assign({}, store.state) !// or Clone or something })

Slide 21

Slide 21 text

state() { return { fridge: [], } }, State by function

Slide 22

Slide 22 text

describe('mutations', () !=> { let state const mutations = store.mutations beforeEach(() !=> { state = store.state() }) test('#addToFridge stores ingredients in the fridge', () !=> { mutations.addToFridge(state, 'ice cream') expect(state.fridge).toEqual(['ice cream']) }) test('#addToFridge also takes numbers', () !=> { mutations.addToFridge(state, 2) expect(state.fridge).toEqual([2]) }) })

Slide 23

Slide 23 text

const collection = [] export const easy = { state() { return { fridge: collection } }, !!... } This breaks your tests

Slide 24

Slide 24 text

CACHE DESIRED FILE FILE 1 FILE 2 First time a file gets required

Slide 25

Slide 25 text

CACHE DESIRED FILE FILE 1 FILE 2 File gets loaded and is cached

Slide 26

Slide 26 text

CACHE DESIRED FILE FILE 1 FILE 2 Same file gets required again

Slide 27

Slide 27 text

CACHE DESIRED FILE FILE 1 FILE 2 File is returned from cache directly

Slide 28

Slide 28 text

jest.resetModules() Load a fresh instance with

Slide 29

Slide 29 text

let store, state, mutations beforeEach(() !=> { store = require('./store') state = store.state() mutations = store.mutations }) afterEach(() !=> { jest.resetModules() }) describe('mutations', () !=> { test('#addToFridge stores ingredients in the fridge', () !=> { … }) test('#addToFridge also takes numbers', () !=> { … }) }) let store, state, mutations beforeEach(() !=> { store = require(‘./store').default state = store.state() mutations = store.mutations }) afterEach(() !=> { jest.resetModules() })

Slide 30

Slide 30 text

Actions

Slide 31

Slide 31 text

export default { state() { return { fridge: collection } }, mutations: { addToFridge(state, ingredient) { … } }, actions: { storeIngredients({ commit }, ingredient) { commit('addToFridge', ingredient) } }, getters: { … } }

Slide 32

Slide 32 text

describe('actions', () !=> { let actions const storeObj = { commit: jest.fn() } beforeEach(() !=> { actions = store.actions }) })

Slide 33

Slide 33 text

describe('actions', () !=> { let actions const storeObj = { commit: jest.fn() } beforeEach(() !=> { actions = store.actions }) test('#storeIngredients', () !=> { actions.storeIngredients(storeObj, 'banana') expect(storeObj.commit).toBeCalledWith('addToFridge', 'banana') }) })

Slide 34

Slide 34 text

describe('actions', () !=> { let actions const storeObj = { commit: jest.fn() } beforeEach(() !=> { actions = store.actions }) test('#storeIngredients', () !=> { actions.storeIngredients(storeObj, 'banana') expect(storeObj.commit).toBeCalledWith('addToFridge', 'banana') }) test('#storeIngredients 2', () !=> { actions.storeIngredients(storeObj, 'froyo') expect(storeObj.commit).toBeCalledWith('addToFridge', ‘froyo') expect(storeObj.commit).toHaveBeenCalledTimes(2) }) })

Slide 35

Slide 35 text

describe('actions', () !=> { let actions const storeObj = { commit: jest.fn() } beforeEach(() !=> { actions = store.actions }) test('#storeIngredients', () !=> { actions.storeIngredients(storeObj, 'banana') expect(storeObj.commit).toBeCalledWith('addToFridge', 'banana') }) test('#storeIngredients 2', () !=> { actions.storeIngredients(storeObj, ‘froyo') expect(storeObj.commit).toBeCalledWith('addToFridge', 'banana') expect(storeObj.commit).toHaveBeenCalledTimes(2) }) }) storeObj.commit.mockReset() Easy Fix

Slide 36

Slide 36 text

async orderIngredient({ commit }, ingredient) { const message = await Promise.resolve(`${ingredient} ordered`) commit('addToFridge', message) return message }, test('#orderIngredients', async () !=> { await actions.orderIngredient(storeObj, 'apple') expect(storeObj.commit).toBeCalledWith('addToFridge', 'apple ordered') })

Slide 37

Slide 37 text

async orderIngredient({ commit }, ingredient) { const message = await axios.get(`/order&ingredient=${ingredient}`) commit('addToFridge', message) } test('#orderIngredients', async () !=> { axios.get.mockResolvedValue('apple ordered') await actions.orderIngredient(storeObj, 'apple') expect(axios.get).toBeCalledWith('/order&ingredient=apple') expect(storeObj.commit).toBeCalledWith('addToFridge', 'apple ordered') }) jest.mock('axios') import axios from 'axios'

Slide 38

Slide 38 text

import axios from 'axios' !// Create a custom axios instance const customAxios = axios.create({ showProgress: true, headers: { 'Content-Type': 'application/json'} }) !// Define special headers that are send automatically !// for certain verbs customAxios.defaults.headers.post['X-CSRF-Token'] = window.csrfToken customAxios.defaults.headers.put['X-CSRF-Token'] = window.csrfToken customAxios.defaults.headers.delete['X-CSRF-Token'] = window.csrfToken customAxios.interceptors.request.use( !//!!... ) export default { get: customAxios.get.bind(customAxios), delete: customAxios.delete.bind(customAxios), put: customAxios.put.bind(customAxios), post: customAxios.post.bind(customAxios), !__axios: customAxios } ajax.js >

Slide 39

Slide 39 text

import axios from 'axios' !// Create a custom axios instance const customAxios = axios.create({ showProgress: true, headers: { 'Content-Type': 'application/json'} }) !// Define special headers that are send automatically !// for certain verbs customAxios.defaults.headers.post['X-CSRF-Token'] = window.csrfToken customAxios.defaults.headers.put['X-CSRF-Token'] = window.csrfToken customAxios.defaults.headers.delete['X-CSRF-Token'] = window.csrfToken customAxios.interceptors.request.use( !//!!... ) export default { get: customAxios.get.bind(customAxios), delete: customAxios.delete.bind(customAxios), put: customAxios.put.bind(customAxios), post: customAxios.post.bind(customAxios), !__axios: customAxios } ajax.js > Custom Mocks!

Slide 40

Slide 40 text

const options = { success: true, data: {} } const stub = () !=> { if (options.success) { return Promise.resolve({ data: options.data }) } else { const err = new Error() err.response = { data: options.data } return Promise.reject(err) } } export default { get: jest.fn(() !=> stub()), delete: jest.fn(() !=> stub()), put: jest.fn(() !=> stub()), post: jest.fn(() !=> stub()), !__setResponse: (data, success = true) !=> { options.success = success options.data = data } } __mocks__/ajax.js >

Slide 41

Slide 41 text

test('success', async () !=> { ajax.!__setResponse({ builds: [1,2,3,4]}) const result = await getSomeBuilds() expect(result).toEqual({ builds: [1,2,3,4]}) }) test('fail', async () !=> { ajax.!__setResponse({ errors: ['could not load'] }, false) expect(() !=> await getSomeBuilds()).toThrow(new Error(…)) })

Slide 42

Slide 42 text

Getters

Slide 43

Slide 43 text

Getters Noooooooooooooooooooooooooooo

Slide 44

Slide 44 text

Components

Slide 45

Slide 45 text

Order {{ orderSize }}! {{ alert }}!

In The Fridge!

  • {{ ing }}!
  • !
!
! !// Expects Store to be there import { mapGetters, mapActions } from 'vuex' export default { name: 'Fridge', data () { return { ingredient: '', alert: '' } }, Example

Slide 46

Slide 46 text

}, props: { orderSize: Number }, methods: { !!...mapActions(['orderIngredient']), async order() { const message = await this.orderIngredient(this.ingredient) this.showAlert(message) this.ingredient = '' }, showAlert(message) { this.alert = `${this.orderSize} ${message}` setTimeout(() !=> { this.alert = '' }, 2000) } }, computed: { !!...mapGetters(['ingredients']) } } ! Example

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

import { mount, createLocalVue } from '@vue/test-utils' import Component from './component' import store from './store' import Vuex from 'vuex' test('Default Template', () !=> { const localVue = createLocalVue() localVue.use(Vuex) const wrapper = mount(Component, { propsData: { orderSize: 10 }, localVue, store: new Vuex.Store(store) }) expect(wrapper.html()).toMatchSnapshot() })

Slide 49

Slide 49 text

expect(wrapper.html()).toMatchSnapshot() expect(wrapper).toMatchSnapshot() npm install --save-dev jest-serializer-vue

Slide 50

Slide 50 text

test('Default Template', () !=> { const localVue = createLocalVue() localVue.use(Vuex) const wrapper = mount(Component, { propsData: { orderSize: 10 }, localVue, store: new Vuex.Store(store) }) expect(wrapper).toMatchSnapshot() }) exports[`Default Template 1`] = `
Order 10

In The Fridge

  • banana
  • chewing gum
  • toffee
`;

Slide 51

Slide 51 text

test(‘Alert Rendered', () !=> { const localVue = createLocalVue() localVue.use(Vuex) const wrapper = mount(Component, { propsData: { orderSize: 10 }, localVue, store: new Vuex.Store(store) }) wrapper.vm.alert = 'Message Should be printed' expect(wrapper).toMatchSnapshot() }) exports[`Alert Rendered 1`] = `
Order 10 Message Should be printed

In The Fridge

  • banana
  • chewing gum
  • toffee
`;

Slide 52

Slide 52 text

Duplication?

Slide 53

Slide 53 text

Factories!

Slide 54

Slide 54 text

function factory() { const localVue = createLocalVue() localVue.use(Vuex) return mount(Component, { propsData: { orderSize: 10, }, localVue, store: new Vuex.Store(store) }) }

Slide 55

Slide 55 text

function factory() { const localVue = createLocalVue() localVue.use(Vuex) return mount(Component, { propsData: { orderSize: 10, }, localVue, store: new Vuex.Store(store) }) } test('Default Template', () !=> { const wrapper = factory() expect(wrapper).toMatchSnapshot() }) test('Action Rendered', () !=> { const wrapper = factory() wrapper.vm.alert = 'Message Should be printed' expect(wrapper).toMatchSnapshot() })

Slide 56

Slide 56 text

function factory(props = {}, options = {}) { const localVue = createLocalVue() localVue.use(Vuex) return mount(Component, { propsData: { orderSize: 10, !!...props }, localVue, store: new Vuex.Store(store), !!...options }) } function factory(props = {}, options = {}) { !!...props !!...options test('Takes Props', () !=> { const wrapper = factory({ orderSize: 20 }) expect(wrapper).toMatchSnapshot() })

Slide 57

Slide 57 text

function storeFactory(store) { const localVue = createLocalVue() localVue.use(Vuex) return { localVue, store: new Vuex.Store(store) } } function factory(props = {}, options = {}) { return mount(Component, { propsData: { orderSize: 10, !!...props }, !!...storeFactory(store), !!...options }) }

Slide 58

Slide 58 text

import { createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' export default function storeFactory(store) { const localVue = createLocalVue() localVue.use(Vuex) return { localVue, store: new Vuex.Store(store) } } import { mount } from '@vue/test-utils' import Component from './component' import store from './store' import storeFactory from './factories/store' function factory(props = {}, options = {}) { return mount(Component, { propsData: { orderSize: 10, !!...props }, !!...storeFactory(store), !!...options }) } factories/store.js

Slide 59

Slide 59 text

export function createFactory(fn) { return function (options = {}) { const defaults = { props: {}, store: {}, options: {} } const params = Object.assign({}, defaults, options) return fn(params) } } factories/index.js

Slide 60

Slide 60 text

export { default as storeFactory } from './store' export function createFactory(fn) { return function (options = {}) { const defaults = { props: {}, store: {}, options: {} } const params = Object.assign({}, defaults, options) return fn(params) } } factories/index.js

Slide 61

Slide 61 text

import { createFactory, storeFactory } from './factories' const factory = createFactory(({ props, options }) !=> { return mount(Component, { propsData: { orderSize: 10, !!...props }, !!...storeFactory(store), !!...options }) }) test('Takes Props', () !=> { const wrapper = factory({ props: { orderSize: 20 }}) expect(wrapper).toMatchSnapshot() })

Slide 62

Slide 62 text

methods: { !!...mapActions(['orderIngredient']), async order() { const message = await this.orderIngredient(this.ingredient) this.showAlert(message) this.ingredient = '' }, showAlert(message) { this.alert = `${this.orderSize} ${message}` setTimeout(() !=> { this.alert = '' }, 5000) } }, test('#order', async () !=> { jest.useFakeTimers(); const { vm } = factory(); vm.ingredient = 'pie'; await vm.order(); expect(vm.alert).toBe('10 pie ordered'); jest.runAllTimers(); expect(vm.alert).toBe(''); });

Slide 63

Slide 63 text

Unit Tests Next Level

Slide 64

Slide 64 text

Acceptance Tests

Slide 65

Slide 65 text

test(‘acceptance', () !=> { !// SETUP APP })

Slide 66

Slide 66 text

test(‘acceptance', () !=> { !// SETUP APP !// STORE INITIAL SNAPSHOT })

Slide 67

Slide 67 text

test('acceptance', () !=> { !// SETUP APP !// STORE INITIAL SNAPSHOT !// FILL INPUT & CLICK BUTTON })

Slide 68

Slide 68 text

test(‘acceptance', () !=> { !// SETUP APP !// STORE INITIAL SNAPSHOT !// FILL INPUT & CLICK BUTTON !// TAKE NEW SNAPSHOT })

Slide 69

Slide 69 text

test(‘acceptance', () !=> { !// SETUP APP !// STORE INITIAL SNAPSHOT !// FILL INPUT & CLICK BUTTON !// TAKE NEW SNAPSHOT !// AWAIT ALERT TO DISAPPEAR })

Slide 70

Slide 70 text

test(‘acceptance', () !=> { !// SETUP APP !// STORE INITIAL SNAPSHOT !// FILL INPUT & CLICK BUTTON !// TAKE NEW SNAPSHOT !// AWAIT ALERT TO DISAPPEAR !// TAKE FINAL SNAPSHOT })

Slide 71

Slide 71 text

test.only('acceptance raw', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') })

Slide 72

Slide 72 text

test.only('acceptance raw', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') expect(app).toMatchSnapshot() })

Slide 73

Slide 73 text

test.only('acceptance raw', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') expect(app).toMatchSnapshot() input.element.value = 'banana' input.trigger('input') button.trigger('click') })

Slide 74

Slide 74 text

test.only('acceptance raw', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') expect(app).toMatchSnapshot() input.element.value = 'banana' input.trigger('input') button.trigger('click') await flushPromises() expect(app).toMatchSnapshot() expect(app.vm.ingredient).toBe('') })

Slide 75

Slide 75 text

test.only('acceptance raw', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') expect(app).toMatchSnapshot() input.element.value = 'banana' input.trigger('input') button.trigger('click') await flushPromises() expect(app).toMatchSnapshot() expect(app.vm.ingredient).toBe('') jest.runAllTimers() expect(app).toMatchSnapshot() })

Slide 76

Slide 76 text

exports[`acceptance raw 1`] = `
Order 12

In The Fridge

  • banana
  • chewing gum
  • toffee
`; exports[`acceptance raw 2`] = `
Order 12 12 banana ordered

In The Fridge

  • banana
  • chewing gum
  • toffee
  • banana ordered
`; exports[`acceptance raw 3`] = `
Order 12

In The Fridge

  • banana
  • chewing gum
  • toffee
  • banana ordered
`;

Slide 77

Slide 77 text

import snapshotDiff from ‘snapshot-diff' import vuesnap from ‘vue-snapshot-serializer’

Slide 78

Slide 78 text

test.only('acceptance diff', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') let current let next let diff current = vuesnap.print(app) expect(current).toMatchSnapshot() input.element.value = 'banana' input.trigger('input') button.trigger('click') await flushPromises() next = vuesnap.print(app) diff = snapshotDiff(current, next) current = next expect(diff).toMatchSnapshot() expect(app.vm.ingredient).toBe('') jest.runAllTimers() next = vuesnap.print(app) diff = snapshotDiff(current, next) expect(diff).toMatchSnapshot() }) import snapshotDiff from ‘snapshot-diff' import vuesnap from ‘vue-snapshot-serializer’ let current let next let diff current = vuesnap.print(app) next = vuesnap.print(app) diff = snapshotDiff(current, next) current = next expect(diff).toMatchSnapshot()

Slide 79

Slide 79 text

exports[`acceptance diff 1`] = `
Order 12

In The Fridge

  • banana
  • chewing gum
  • toffee
  • banana ordered
`; exports[`acceptance diff 2`] = ` "Snapshot Diff: - First value + Second value -
Order 12 +
Order 12 12 banana ordered

In The Fridge

  • banana
  • chewing gum
  • toffee
  • +
  • banana ordered
" `; exports[`acceptance diff 3`] = ` "Snapshot Diff: - First value + Second value @@ -1,6 +1,6 @@ -
Order 12 12 banana ordered +
Order 12

In The Fridge

  • banana
  • chewing gum
  • " `;

Slide 80

Slide 80 text

Jest-acceptance https://github.com/codeship/jest-acceptance

Slide 81

Slide 81 text

test('acceptance', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const tester = new Tester(app) })

Slide 82

Slide 82 text

test('acceptance', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const tester = new Tester(app) expect(tester.current()).toMatchSnapshot() })

Slide 83

Slide 83 text

test('acceptance', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const tester = new Tester(app) expect(tester.current()).toMatchSnapshot() tester.fillIn('ingredient', 'banana') tester.click('button') })

Slide 84

Slide 84 text

test('acceptance', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const tester = new Tester(app) expect(tester.current()).toMatchSnapshot() tester.fillIn('ingredient', 'banana') tester.click('button') await flushPromises() expect(tester.next()).toMatchSnapshot() expect(app.vm.ingredient).toBe('') })

Slide 85

Slide 85 text

test('acceptance', async () !=> { jest.useFakeTimers() const app = factory({ props: { orderSize: 12 } }) const tester = new Tester(app) expect(tester.current()).toMatchSnapshot() tester.fillIn('ingredient', 'banana') tester.click('button') await flushPromises() expect(tester.next()).toMatchSnapshot() expect(app.vm.ingredient).toBe('') jest.runAllTimers() expect(tester.next()).toMatchSnapshot() })

Slide 86

Slide 86 text

exports[`acceptance 1`] = `
Order 12

In The Fridge

  • banana
  • chewing gum
  • toffee
`; exports[`acceptance 2`] = ` - Diff A + Diff B -
Order 12 +
Order 12 12 banana ordered

In The Fridge

  • banana
  • chewing gum
  • toffee
  • +
  • banana ordered
`; exports[`acceptance 3`] = ` - Diff A + Diff B @@ -1,6 +1,6 @@ -
Order 12 12 banana ordered +
Order 12

In The Fridge

  • banana
  • chewing gum
  • `;

Slide 87

Slide 87 text

Mähhh* @codebryo @codeship @cloudbees *Thank you!