Unit testing in Vue with @vue/test-utils

What should we test?

Component output/external contracts

- rendered template

- emitted events

- any side effects (imported functions, Vuex actions, router events, etc.)

What should we NOT test?

Component internals

- if the component method is called

- output of computed property outside the template

- any changes to data properties if checked directly etc

Writing the very first component test

// MyComponent.spec.js

import { shallowMount } from "@vue/test-utils";
import MyComponent from "@/components/MyComponent.vue";

describe("MyComponent", () => {
  let wrapper;

  it("is a Vue component", () => {
    wrapper = shallowMount(MyComponent);

    expect(wrapper.exists()).toBe(true);
  });
});

Isolating test cases

// MyComponent.spec.js

// component factory
function createComponent() {
  wrapper = shallowMount(MyComponent);
}

// destroying a wrapper
afterEach(() => {
  wrapper.destroy();
});

Testing conditional rendering

// MyComponent.spec.js

it('does not render title when showTitle prop is false', () => {
  createComponent()

  expect(wrapper.find("[data-testid='title']").exists()).toBe(false)
})

Passing component props

function createComponent({ props = {} } = {}) {
  wrapper = shallowMount(MyComponent, {
    propsData: {
      ...props,
    },
  })
}

...

createComponent({ showTitle: true })

Abstracting finder helpers

let wrapper

const findTitle = () => wrapper.find("[data-testid='title']")

Triggering native events

const findTitle = () => wrapper.find("[data-testid='title']")
const findIncrementButton = () => wrapper.find('.test-increment-button')

...

it('increments count on increment button click', () => {
  createComponent()

  expect(findCount().text()).toBe('Count: 0')
  findIncrementButton().trigger('click')
})

Dealing with DOM updates

const findTitle = () => wrapper.find("[data-testid='title']")
const findIncrementButton = () => wrapper.find('.test-increment-button')

...

it('increments count on increment button click', async () => {
  createComponent()

  expect(findCount().text()).toBe('Count: 0')
  findIncrementButton().trigger('click')
  await nextTick()
  expect(findCount().text()).toBe('Count: 1')
})

Testing computed property

Rule of thumb: follow the user!

Testing computed property

  it('renders a correct doubled value', () => {
    createComponent()

    expect(findDoubleCount().text()).toContain('0')
  })

Setting component data

function createComponent({ props = {}, data = {} } = {}) {
  wrapper = shallowMount(MyComponent, {
    propsData: {
      ...props,
    },
    data() {
      return {
        ...data,
      }
    },
  })
}

Setting component data

it('renders a correct doubled value', () => {
  createComponent({ data: { count: 3 } })

  expect(findDoubleCount().text()).toContain('6')
})

Checking component emitted events

it('emits a custom event on emitter button click', () => {
  createComponent()
  wrapper.find('.test-emitter').trigger('click')

  expect(wrapper.emitted('custom-event')).toBeTruthy()
  expect(wrapper.emitted('custom-event')).toEqual([['Hello World']])
})

Testing a watcher

it('emits an event on showTitle change', async () => {
  createComponent()

  wrapper.setProps({ showTitle: true })
  await nextTick()
  expect(wrapper.emitted('watcher-triggered')).toBeTruthy()
})

Dealing with child components

- shallowMount vs. mount

- deal with component as a black box

- remember to work with component contracts: props and events

Dealing with child components

it('changes childCounter on MyButton click', async () => {
  createComponent()

  wrapper.findComponent(MyButton).vm.$emit('click')
  await nextTick()
  expect(findChildCounter().text()).toContain('2')
})

Router

function createComponent({ props = {}, data = {} } = {}) {
  wrapper = shallowMount(MyComponent, {
    propsData: {
      ...props,
    },
    data() {
      return {
        ...data,
      }
    },
    mocks: {
      $route: {
        path: '/',
      },
    },
    stubs: ['router-link'],
  })
}

Router

import { shallowMount } from '@vue/test-utils'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [{ path: '/', component: MyComponent }],
})


function createComponent({ props = {}, data = {} } = {}) {
  wrapper = shallowMount(MyComponent, {
    router,
    ...
  })
}

Vuex in components

import store from '@/store'
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

describe('MyComponent', () => {
  let wrapper
  let mockStore
  
  function createComponent({ props = {}, data = {} } = {}) {
    wrapper = shallowMount(MyComponent, {
      router,
      store: mockStore
      ...
    })
  }
  
  beforeEach(() => {
    mockStore = Vuex.Store(store)
  })
})

Vuex mutations

import { mutations } from '@/store'

describe('mutations', () => {
  let state

  it('updateUsername', () => {
    const newUsername = 'Test2'

    state = {
      username: 'Test',
    }

    mutations.updateUsername(state, newUsername)
    expect(state.username).toBe(newUsername)
  })
})

Vue Testing Workshop

By Natalia Tepluhina

Vue Testing Workshop

Smashing Workshop - Day 5

  • 704