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