Save 37% off PRO during our Black Friday Sale! »

Next Level Jest Testing for Vue

9b3ce79a4e2f5656234300be6c321f88?s=47 Roman Kuba
February 15, 2019

Next Level Jest Testing for Vue

Learn some tricks in how to improve your test coverage with Jest and solve seemingly complex issues with ease. You can even run "acceptance tests" with Jest. Did you know?

9b3ce79a4e2f5656234300be6c321f88?s=128

Roman Kuba

February 15, 2019
Tweet

Transcript

  1. None
  2. Hi, I’m Roman! @codebryo That’s where I work

  3. Jumped on Vue with 0.11

  4. revue vue-test-utils ? jest

  5. Snapshots Run your code Snapshot the piece You care for.

    Store it
  6. 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", } `;
  7. Randomnessssss

  8. Randomnessssss

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

  10. import faker from 'faker' const RandomUser = { name: faker.name.findName(),

    job: faker.name.jobTitle(), age: faker.random.number({ min: 60}) } Property Matchers + Snapshots
  11. 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) }) })
  12. exports[`RandomUser snapshot 1`] = ` Object { "age": Any<Number>, "job":

    Any<String>, "name": Any<String>, } `;
  13. None
  14. export default { state: { !// plain Object }, mutations:

    { !// simple functions }, actions: { !// special functions that invoke other functions }, getters: { !// computed properties } }
  15. export default { state: { fridge: [], }, mutations: {

    addToFridge(state, ingredient) { state.fridge.push(ingredient) } }, actions: { }, getters: { } }
  16. 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<Array>, } `;
  17. 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']) }) })
  18. 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']) }) })
  19. [‘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]) }) })
  20. 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 })
  21. state() { return { fridge: [], } }, State by

    function
  22. 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]) }) })
  23. const collection = [] export const easy = { state()

    { return { fridge: collection } }, !!... } This breaks your tests
  24. CACHE DESIRED FILE FILE 1 FILE 2 First time a

    file gets required
  25. CACHE DESIRED FILE FILE 1 FILE 2 File gets loaded

    and is cached
  26. CACHE DESIRED FILE FILE 1 FILE 2 Same file gets

    required again
  27. CACHE DESIRED FILE FILE 1 FILE 2 File is returned

    from cache directly
  28. jest.resetModules() Load a fresh instance with

  29. 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() })
  30. Actions

  31. export default { state() { return { fridge: collection }

    }, mutations: { addToFridge(state, ingredient) { … } }, actions: { storeIngredients({ commit }, ingredient) { commit('addToFridge', ingredient) } }, getters: { … } }
  32. describe('actions', () !=> { let actions const storeObj = {

    commit: jest.fn() } beforeEach(() !=> { actions = store.actions }) })
  33. 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') }) })
  34. 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) }) })
  35. 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
  36. 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') })
  37. 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'
  38. 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 >
  39. 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!
  40. 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 >
  41. 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(…)) })
  42. Getters

  43. Getters Noooooooooooooooooooooooooooo

  44. Components

  45. <template> <div> <input type="text" v-model="ingredient"!/> <button @click="order">Order {{ orderSize }}!</button>

    <span>{{ alert }}!</span> <hr> <h4>In The Fridge!</h4> <ul> <li v-for="ing in ingredients">{{ ing }}!</li> !</ul> !</div> !</template> <script> !// Expects Store to be there import { mapGetters, mapActions } from 'vuex' export default { name: 'Fridge', data () { return { ingredient: '', alert: '' } }, Example
  46. }, 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']) } } !</script> Example
  47. None
  48. 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() })
  49. expect(wrapper.html()).toMatchSnapshot() expect(wrapper).toMatchSnapshot() npm install --save-dev jest-serializer-vue

  50. 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`] = ` <div> <input type="text"> <button>Order 10</button> <span></span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> </ul> </div> `;
  51. 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`] = ` <div> <input type="text"> <button>Order 10</button> <span>Message Should be printed</span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> </ul> </div> `;
  52. Duplication?

  53. Factories!

  54. function factory() { const localVue = createLocalVue() localVue.use(Vuex) return mount(Component,

    { propsData: { orderSize: 10, }, localVue, store: new Vuex.Store(store) }) }
  55. 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() })
  56. 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() })
  57. 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 }) }
  58. 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
  59. export function createFactory(fn) { return function (options = {}) {

    const defaults = { props: {}, store: {}, options: {} } const params = Object.assign({}, defaults, options) return fn(params) } } factories/index.js
  60. 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
  61. 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() })
  62. 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(''); });
  63. Unit Tests Next Level

  64. Acceptance Tests

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

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

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

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

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

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

    SNAPSHOT !// FILL INPUT & CLICK BUTTON !// TAKE NEW SNAPSHOT !// AWAIT ALERT TO DISAPPEAR !// TAKE FINAL SNAPSHOT })
  71. test.only('acceptance raw', async () !=> { jest.useFakeTimers() const app =

    factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') })
  72. 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() })
  73. 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') })
  74. 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('') })
  75. 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() })
  76. exports[`acceptance raw 1`] = ` <div><input type="text" name="ingredient"> <button>Order 12</button>

    <span></span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> </ul> </div> `; exports[`acceptance raw 2`] = ` <div><input type="text" name="ingredient"> <button>Order 12</button> <span>12 banana ordered</span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> <li>banana ordered</li> </ul> </div> `; exports[`acceptance raw 3`] = ` <div><input type="text" name="ingredient"> <button>Order 12</button> <span></span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> <li>banana ordered</li> </ul> </div> `;
  77. import snapshotDiff from ‘snapshot-diff' import vuesnap from ‘vue-snapshot-serializer’

  78. 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()
  79. exports[`acceptance diff 1`] = ` <div><input type="text" name="ingredient"> <button>Order 12</button>

    <span></span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> <li>banana ordered</li> </ul> </div> `; exports[`acceptance diff 2`] = ` "Snapshot Diff: - First value + Second value - <div><input type=\\"text\\" name=\\"ingredient\\"> <button>Order 12</button> <span></span> + <div><input type=\\"text\\" name=\\"ingredient\\"> <button>Order 12</button> <span>12 banana ordered</span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> + <li>banana ordered</li> </ul> </div>" `; exports[`acceptance diff 3`] = ` "Snapshot Diff: - First value + Second value @@ -1,6 +1,6 @@ - <div><input type=\\"text\\" name=\\"ingredient\\"> <button>Order 12</button> <span>12 banana ordered</span> + <div><input type=\\"text\\" name=\\"ingredient\\"> <button>Order 12</button> <span></span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li>" `;
  80. Jest-acceptance https://github.com/codeship/jest-acceptance

  81. test('acceptance', async () !=> { jest.useFakeTimers() const app = factory({

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

    props: { orderSize: 12 } }) const tester = new Tester(app) expect(tester.current()).toMatchSnapshot() })
  83. 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') })
  84. 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('') })
  85. 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() })
  86. exports[`acceptance 1`] = ` <div><input type="text" name="ingredient"> <button>Order 12</button> <span></span>

    <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> </ul> </div> `; exports[`acceptance 2`] = ` - Diff A + Diff B - <div><input type="text" name="ingredient"> <button>Order 12</button> <span></span> + <div><input type="text" name="ingredient"> <button>Order 12</button> <span>12 banana ordered</span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> <li>toffee</li> + <li>banana ordered</li> </ul> </div> `; exports[`acceptance 3`] = ` - Diff A + Diff B @@ -1,6 +1,6 @@ - <div><input type="text" name="ingredient"> <button>Order 12</button> <span>12 banana ordered</span> + <div><input type="text" name="ingredient"> <button>Order 12</button> <span></span> <hr> <h4>In The Fridge</h4> <ul> <li>banana</li> <li>chewing gum</li> `;
  87. Mähhh* @codebryo @codeship @cloudbees *Thank you!