Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Next Level Jest Testing for Vue

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?

Roman Kuba

February 15, 2019
Tweet

More Decks by Roman Kuba

Other Decks in Programming

Transcript

  1. 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", } `;
  2. import faker from 'faker' const RandomUser = { name: faker.name.findName(),

    job: faker.name.jobTitle(), age: faker.random.number({ min: 60}) } Property Matchers + Snapshots
  3. 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) }) })
  4. export default { state: { !// plain Object }, mutations:

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

    addToFridge(state, ingredient) { state.fridge.push(ingredient) } }, actions: { }, getters: { } }
  6. 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>, } `;
  7. 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']) }) })
  8. 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']) }) })
  9. [‘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]) }) })
  10. 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 })
  11. 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]) }) })
  12. const collection = [] export const easy = { state()

    { return { fridge: collection } }, !!... } This breaks your tests
  13. 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() })
  14. export default { state() { return { fridge: collection }

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

    commit: jest.fn() } beforeEach(() !=> { actions = store.actions }) })
  16. 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') }) })
  17. 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) }) })
  18. 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
  19. 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') })
  20. 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'
  21. 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 >
  22. 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!
  23. 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 >
  24. 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(…)) })
  25. <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
  26. }, 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
  27. 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() })
  28. 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> `;
  29. 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> `;
  30. function factory() { const localVue = createLocalVue() localVue.use(Vuex) return mount(Component,

    { propsData: { orderSize: 10, }, localVue, store: new Vuex.Store(store) }) }
  31. 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() })
  32. 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() })
  33. 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 }) }
  34. 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
  35. export function createFactory(fn) { return function (options = {}) {

    const defaults = { props: {}, store: {}, options: {} } const params = Object.assign({}, defaults, options) return fn(params) } } factories/index.js
  36. 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
  37. 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() })
  38. 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(''); });
  39. test('acceptance', () !=> { !// SETUP APP !// STORE INITIAL

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

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

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

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

    factory({ props: { orderSize: 12 } }) const input = app.find('[name="ingredient"]') const button = app.find('button') })
  44. 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() })
  45. 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') })
  46. 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('') })
  47. 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() })
  48. 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> `;
  49. 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()
  50. 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>" `;
  51. test('acceptance', async () !=> { jest.useFakeTimers() const app = factory({

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

    props: { orderSize: 12 } }) const tester = new Tester(app) expect(tester.current()).toMatchSnapshot() })
  53. 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') })
  54. 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('') })
  55. 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() })
  56. 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> `;