Mocking Vuex in Vue unit tests

Lachlan Miller
6 min readNov 14, 2017

--

Vue is a UI library — so naturally, testing Vue components usually involves asserts whether the UI correctly reflects the state of the application, or in this case, the data supplied from computed, data or in larger apps, the Vuex store.

Most of the time, however, you want to _avoid_ using Vuex in component tests. Let me explain why, and some best practises I learned while working on several large Vue/Vuex applications.

The source code for this article is here.

Check out my Vue.js 3 course! We cover the Composition API, TypeScript, Unit Testing, Vuex and Vue Router.

Testing components that rely on $store.state

Vuex provides four main APIs for interacting with the store — the state , mutations , actions , and getters . Let’s look at state first.

The below component shows the three usual ways you will access $store.state : directly using {{ $store.state }} (which I don’t like, see why below), using a computed property to return a state property, or using the mapState helper.

<template>
<div>
<div class="state-1">
{{ $store.state.value_1 }}
</div>
<div class="state-2">
{{ value_2 }}
</div>
<div class="state-3">
{{ value_3 }}
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'State',
computed: {
value_2 () {
return this.$store.state.value_2
},
...mapState({
value_3: state => state.value_3
})
}
}
</script>

Below shows how to test these. You can see why calling {{ $store }} directly in the markup is bothersome — it returns you to fully mock the store, which you shouldn’t need to assert the UI behaves in a certain way based on the state. When testing if the component renders correctly based on some state, we don’t care where the data comes from — only that the UI is correctly reflecting it.

import Vuex from 'vuex'
import { shallow, createLocalVue } from 'vue-test-utils'
import State from './State.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('State', () => {
it('renders a value from $store.state', () => {
const wrapper = shallow(State, {
mocks: {
$store: {
state: {
value_1: 'value_1'
}
}
},
localVue
})
expect(wrapper.find('.state-1')
.text().trim()).toEqual('value_1')
})
it('renders a $store.state value return from computed', () => {
const wrapper = shallow(State, {
computed: {
value_2: () => 'value_2'
},
localVue
})
expect(wrapper.find('.state-2')
.text().trim()).toEqual('value_2')
})
it('renders a $store.state value return from mapState', () => {
const wrapper = shallow(State, {
computed: {
value_3: () => 'value_3'
},
localVue
})
expect(wrapper.find('.state-3')
.text().trim()).toEqual('value_3')
})
})

The main takeaway here is that you can mock the data using the computed option for shallow and mount .

Testing components that store.$commit mutations

When a user interacts with a component that commits a mutation, you want to test the following things:

  1. does the mutation get committed?
  2. does it have the correct payload?

That’s it. Component tests are not the place to assert the state was actually modified — you should test that in isolation.

Here is a simple component that commits a mutation when a button is clicked:

<template>
<div>
<button @click="handle">Button</button>
</div>
</template>
<script>
export default {
name: 'Mutations',
data () {
return {
val: 'val'
}
},
methods: {
handle () {
this.$store.commit('MUTATION', { val: this.val })
}
}
}
</script>

And the test.

import Vuex from 'vuex'
import { shallow, createLocalVue } from 'vue-test-utils'
import Mutations from './Mutations.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Mutations', () => {
let store
let mutations
beforeEach(() => {
mutations = {
MUTATION: jest.fn()
}
store = new Vuex.Store({
mutations
})
})
it('commits a MUTATION type mutation when a button is clicked', () => {
const wrapper = shallow(Mutations, {
store,
localVue
})
wrapper.find('button').trigger('click') expect(mutations.MUTATION.mock.calls).toHaveLength(1)
expect(mutations.MUTATION.mock.calls[0][1])
.toEqual({ val: 'val' })
})
})

It’s better to use a real store when asserts mutations are committed, so you can not only asserts the method is called correctly, but the correct payload is passed. Note you have to do calls[0][1] — mutations always receive the state as the first argument, and the payload as the second.

You can do the same for mapCommit, but I have never found a use for this helper, so I didn’t show it.

Testing components that rely on $store.getters

Testing components that rely on $store.getters is easy, and similar to $store.state since getters are basically just computed properties for the store. There are a few ways to do this. First, the below component shows two different ways you might use getters — the mapGetters helper, a computed property that also passes and argument.

<template>
<div>
<div class="map-getters">
{{ getter_1 }}
</div>
<div class="computed-getters">
{{ getter_2 }}
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'

export default {
name: 'Getters',
computed: {
...mapGetters({
getter_1: 'getter_1'
}),
getter_2 () {
return this.$store.getters['getter_2']('value_2')
}
}
}
</script>

Testing getters with a store

import Vuex from 'vuex'
import { shallow, createLocalVue } from 'vue-test-utils'
import Getters from './Getters.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Getters', () => {
describe('with a store', () => {
let store
let getters
beforeEach(() => {
getters = {
getter_1: () => 'value_1',
getter_2: () => (arg) => arg
}
store = new Vuex.Store({ getters })
})
it('renders a values from getters', () => {
const wrapper = shallow(Getters, {
store,
localVue
})
expect(wrapper.find('.map-getters')
.text().trim()).toEqual('value_1')
expect(wrapper.find('.computed-getters')
.text().trim()).toEqual('value_2')
})
})
})

Pretty simple. Main points to remember are you can make a fresh store by doing let store outside the describe block, and inside a beforeEachhook create a new store. If you want to change the value of a getter for a particular test, you can use store.hotUpdate like so:

it('shows how to use store.hotUpdate', () => {
store.hotUpdate({
getters: {
...getters,
getter_1: () => 'wrong_value'
}
})
const wrapper = shallow(Getters, {
store,
localVue
})
expect(wrapper.find('.map-getters')
.text().trim()).not.toEqual('value_1')
})
})

Ensuring you use the spread operator ... to supply the rest of the getters in the initial state.

However… this is not always ideal, for a few reasons. We shouldn’t be testing whether the Vuex getters and mapGetters work — test your code, not the framework. There is another option:

Testing getters without a store

describe('without a store', () => {
it('renders a value from getters', () => {
const wrapper = shallow(Getters, {
localVue,
computed: {
getter_1: () => 'value_1',
getter_2: () => 'value_2'
}
})
expect(wrapper.find('.map-getters')
.text().trim()).toEqual('value_1')
expect(wrapper.find('.computed-getters')
.text().trim()).toEqual('value_2')
})
})

getters are just computed values for the store, which are turned into regular computed properties by Vuex. The main point here is test that the UI represents the state of the application correctly — we don’t care where the data comes from, just that it is represented correctly, so there is an advantage to removing the getters/store combo entirely, and to simply set the values we want using the computed option.

Testing components that $store.dispatch actions

This is very similar to testing $store.commit . Again, we are not testing what happens after the action is dispatched — just that it is dispatches, and with the correct payload.

This component dispatches an action when a watch value changes, triggering a dispatch, or when a button is clicked:

<template>
<div>
<button @click="go" />
</div>
</template>
<script>
export default {
name: 'Actions',
data () {
return {
val: 1
}
},
methods: {
go () {
this.$store.dispatch('someAction', { val: this.val })
}
},
watch: {
'val' (val, oldVal) {
this.$store.dispatch('someAction', { val })
}
}
}
</script>

And the test:

import Vuex from 'vuex'
import { shallow, createLocalVue } from 'vue-test-utils'
import Actions from './Actions.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Actions', () => {
let store
let actions
beforeEach(() => {
actions = {
someAction: jest.fn()
}
store = new Vuex.Store({
actions
})
})
it('dispatches an action when a watched value changes', () => {
const wrapper = shallow(Actions, {
store,
localVue
})
wrapper.find('button').trigger('click') expect(actions.someAction.mock.calls).toHaveLength(1)
expect(actions.someAction.mock.calls[0][1]).toEqual({ val: 1 })
})
it('dispatches an action when a watched value changes', async () => {
const wrapper = shallow(Actions, {
store,
localVue
})
await wrapper.setData({ val: 2 }) expect(actions.someAction.mock.calls).toHaveLength(1)
expect(actions.someAction.mock.calls[0][1]).toEqual({ val: 2 })
})
})

Note, since $watch is called asynchronously, you need to mark the test async and await the setData call. A button click is synchronous, however, so you can just write it like any old test.

Most Vue component testing revolves around these ideas — set up the data, and assert the UI is representing it correctly, or verifying an event causes some other method to be called, or some value set.

The smallest and less smart you make your components, the easier they are to test. I typically have some “smart” or “container” components, which have a lot of tests ensuring various things happen when the user interacts, and then just pass the data to props to “dumb” or “regular” components — those are the easiest to test, since their state is mostly dependent on the props you pass them.

Thinking about what components you will need and how you will test them beforehand can help improve your application architecture, and make it more maintainable and testable in the long run. I will go more in depth in to this idea in a future post.

The source code for this article is here.

--

--

Lachlan Miller
Lachlan Miller

Written by Lachlan Miller

I write about frontend, Vue.js, and TDD on https://vuejs-course.com. You can reach me on @lmiller1990 on Github and @Lachlan19900 on Twitter.