Mocking Vuex in Vue unit tests
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.
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:
- does the mutation get committed?
- 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 beforeEach
hook 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.