You’re browsing the documentation for Vue Test Utils for Vue v2.x and earlier.
To read docs for Vue Test Utils for Vue 3, click here.
Guides
Getting Started
What is Vue Test Utils?
Vue Test Utils (VTU) is a set of utility functions aimed to simplify testing Vue.js components. It provides some methods to mount and interact with Vue components in an isolated manner.
Let's see an example:
// Import the `mount()` method from Vue Test Utils
import { mount } from '@vue/test-utils'
// The component to test
const MessageComponent = {
template: '<p>{{ msg }}</p>',
props: ['msg']
}
test('displays message', () => {
// mount() returns a wrapped Vue component we can interact with
const wrapper = mount(MessageComponent, {
propsData: {
msg: 'Hello world'
}
})
// Assert the rendered text of the component
expect(wrapper.text()).toContain('Hello world')
})
Mounted components are returned inside a Wrapper, which exposes methods for querying and interacting with the component under testing.
Simulating User Interaction
Let's imagine a counter component that increments when user clicks the button:
const Counter = {
template: `
<div>
<button @click="count++">Add up</button>
<p>Total clicks: {{ count }}</p>
</div>
`,
data() {
return { count: 0 }
}
}
To simulate the behavior, we need to first locate the button with wrapper.find()
, which returns a wrapper for the button element. We can then simulate the click by calling .trigger()
on the button wrapper:
test('increments counter value on click', async () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
const text = wrapper.find('p')
expect(text.text()).toContain('Total clicks: 0')
await button.trigger('click')
expect(text.text()).toContain('Total clicks: 1')
})
Notice how the test must be async
and that trigger
needs to be awaited. Check out the Testing Asynchronous Behavior guide to understand why this is needed and other things to consider when testing asynchronous scenarios.
What's Next
Check out our common tips when writing tests.
Alternatively, you can explore the full API.
Common Tips
Knowing What to Test
For UI components, we don't recommend aiming for complete line-based coverage, because it leads to too much focus on the internal implementation details of the components and could result in brittle tests.
Instead, we recommend writing tests that assert your component's public interface, and treat its internals as a black box. A single test case would assert that some input (user interaction or change of props) provided to the component results in the expected output (render result or emitted custom events).
For example, imagine a Counter
component which increments a display counter by 1 each time a button is clicked. Its test case would simulate the click and assert that the rendered output has increased by 1. The test should not care about how the Counter
increments the value – it only cares about the input and the output.
The benefit of this approach is that as long as your component's public interface remains the same, your tests will pass no matter how the component's internal implementation changes over time.
This topic is discussed with more details in a great presentation by Matt O'Connell.
Shallow mounting
Sometimes, mounting a whole component with all its dependencies might become slow or cumbersome. For example, components that contain many child components.
Vue Test Utils allows you to mount a component without rendering its child components (by stubbing them) with the shallowMount
method.
import { shallowMount } from '@vue/test-utils'
import Component from '../Component.vue'
const wrapper = shallowMount(Component)
Like mount, it creates a Wrapper that contains the mounted and rendered Vue component, but with stubbed child components.
Notice that using shallowMount
will make the component under testing different from the component you run in your application – some of its parts won't be rendered! This is why it is not the suggested way of testing components unless you face performance issues or need to simplify test arrangements.
Lifecycle Hooks
When using either the mount
or shallowMount
methods, you can expect your component to respond to all lifecycle events. However, it is important to note that beforeDestroy
and destroyed
will not be triggered unless the component is manually destroyed using Wrapper.destroy()
.
Additionally, the component will not be automatically destroyed at the end of each spec, and it is up to the user to stub or manually clean up tasks that will continue to run (setInterval
or setTimeout
, for example) before the end of each spec.
Writing asynchronous tests (new)
By default, Vue batches updates to run asynchronously (on the next "tick"). This is to prevent unnecessary DOM re-renders, and watcher computations (see the docs for more details).
This means that you must wait for updates to run after you change a reactive property. You can do that by awaiting mutation methods like trigger
:
it('updates text', async () => {
const wrapper = mount(Component)
await wrapper.trigger('click')
expect(wrapper.text()).toContain('updated')
await wrapper.trigger('click')
expect(wrapper.text()).toContain('some different text')
})
// Or if you're without async/await
it('render text', done => {
const wrapper = mount(TestComponent)
wrapper.trigger('click').then(() => {
expect(wrapper.text()).toContain('updated')
wrapper.trigger('click').then(() => {
expect(wrapper.text()).toContain('some different text')
done()
})
})
})
Learn more in the Testing Asynchronous Behavior
Asserting Emitted Events
Each mounted wrapper automatically records all events emitted by the underlying Vue instance. You can retrieve the recorded events using the wrapper.emitted()
method:
wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)
/*
`wrapper.emitted()` returns the following object:
{
foo: [[], [123]]
}
*/
You can then make assertions based on these data:
// assert event has been emitted
expect(wrapper.emitted().foo).toBeTruthy()
// assert event count
expect(wrapper.emitted().foo.length).toBe(2)
// assert event payload
expect(wrapper.emitted().foo[1]).toEqual([123])
You can also get an Array of the events in their emit order by calling wrapper.emittedByOrder()
.
Emitting Event from Child Component
You can emit a custom event from a child component by accessing the instance.
Component under test
<template>
<div>
<child-component @custom="onCustom" />
<p v-if="emitted">Emitted!</p>
</div>
</template>
<script>
import ChildComponent from './ChildComponent'
export default {
name: 'ParentComponent',
components: { ChildComponent },
data() {
return {
emitted: false
}
},
methods: {
onCustom() {
this.emitted = true
}
}
}
</script>
Test
import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent'
import ChildComponent from '@/components/ChildComponent'
describe('ParentComponent', () => {
it("displays 'Emitted!' when custom event is emitted", () => {
const wrapper = mount(ParentComponent)
wrapper.findComponent(ChildComponent).vm.$emit('custom')
expect(wrapper.html()).toContain('Emitted!')
})
})
Manipulating Component State
You can directly manipulate the state of the component using the setData
or setProps
method on the wrapper:
it('manipulates state', async () => {
await wrapper.setData({ count: 10 })
await wrapper.setProps({ foo: 'bar' })
})
Mocking Props
You can pass props to the component using Vue's built-in propsData
option:
import { mount } from '@vue/test-utils'
mount(Component, {
propsData: {
aProp: 'some value'
}
})
You can also update the props of an already-mounted component with the wrapper.setProps({})
method.
For a full list of options, please see the mount options section of the docs.
Mocking Transitions
Although calling await Vue.nextTick()
works well for most use cases, there are some situations where additional workarounds are required. These issues will be solved before the vue-test-utils
library moves out of beta. One such example is unit testing components with the <transition>
wrapper provided by Vue.
<template>
<div>
<transition>
<p v-if="show">Foo</p>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: true
}
}
}
</script>
You might want to write a test that verifies that Foo is shown, then when show
is set to false
, Foo is no longer rendered. Such a test could be written as follows:
test('should render Foo, then hide it', async () => {
const wrapper = mount(Foo)
expect(wrapper.text()).toMatch(/Foo/)
await wrapper.setData({
show: false
})
expect(wrapper.text()).not.toMatch(/Foo/)
})
In practice, although we are calling and awaiting setData
to ensure the DOM is updated, this test fails. This is an ongoing issue related to how Vue implements the <transition>
component, that we would like to solve before version 1.0. For now, there are some workarounds:
transitionStub
helper
Using a const transitionStub = () => ({
render: function (h) {
return this.$options._renderChildren
}
})
test('should render Foo, then hide it', async () => {
const wrapper = mount(Foo, {
stubs: {
transition: transitionStub()
}
})
expect(wrapper.text()).toMatch(/Foo/)
await wrapper.setData({
show: false
})
expect(wrapper.text()).not.toMatch(/Foo/)
})
This overrides the default behavior of the <transition>
component and renders the children as soon as the relevant boolean condition changes, as opposed to applying CSS classes, which is how Vue's <transition>
component works.
setData
Avoid Another alternative is to simply avoid using setData
by writing two tests, with the required setup performed using mount
or shallowMount
options:
test('should render Foo', async () => {
const wrapper = mount(Foo, {
data() {
return {
show: true
}
}
})
expect(wrapper.text()).toMatch(/Foo/)
})
test('should not render Foo', async () => {
const wrapper = mount(Foo, {
data() {
return {
show: false
}
}
})
expect(wrapper.text()).not.toMatch(/Foo/)
})
Applying Global Plugins and Mixins
Some of the components may rely on features injected by a global plugin or mixin, for example vuex
and vue-router
.
If you are writing tests for components in a specific app, you can setup the same global plugins and mixins once in the entry of your tests. But in some cases, for example testing a generic component suite that may get shared across different apps, it's better to test your components in a more isolated setup, without polluting the global Vue
constructor. We can use the createLocalVue
method to achieve that:
import { createLocalVue, mount } from '@vue/test-utils'
// create an extended `Vue` constructor
const localVue = createLocalVue()
// install plugins as normal
localVue.use(MyPlugin)
// pass the `localVue` to the mount options
mount(Component, {
localVue
})
Note some plugins, like Vue Router, add read-only properties to the global Vue constructor. This makes it impossible to reinstall the plugin on a localVue
constructor, or add mocks for these read-only properties
Mocking Injections
Another strategy for injected props is simply mocking them. You can do that with the mocks
option:
import { mount } from '@vue/test-utils'
const $route = {
path: '/',
hash: '',
params: { id: '123' },
query: { q: 'hello' }
}
mount(Component, {
mocks: {
// adds mocked `$route` object to the Vue instance
// before mounting component
$route
}
})
Stubbing components
You can override components that are registered globally or locally by using the stubs
option:
import { mount } from '@vue/test-utils'
mount(Component, {
// Will resolve globally-registered-component with
// empty stub
stubs: ['globally-registered-component']
})
Dealing with Routing
Since routing by definition has to do with the overall structure of the application and involves multiple components, it is best tested via integration or end-to-end tests. For individual components that rely on vue-router
features, you can mock them using the techniques mentioned above.
Detecting styles
Your test can only detect inline styles when running in jsdom
.
Testing Key, Mouse and other DOM events
Trigger events
The Wrapper
exposes an async trigger
method. It can be used to trigger DOM events.
test('triggers a click', async () => {
const wrapper = mount(MyComponent)
await wrapper.trigger('click')
})
You should be aware that the find
method returns a Wrapper
as well. Assuming MyComponent
contains a button, the following code clicks the button.
test('triggers a click', async () => {
const wrapper = mount(MyComponent)
await wrapper.find('button').trigger('click')
})
Options
The trigger
method takes an optional options
object. The properties in the options
object are added to the Event.
Note that target cannot be added in the options
object.
test('triggers a click', async () => {
const wrapper = mount(MyComponent)
await wrapper.trigger('click', { button: 0 })
})
Mouse Click Example
Component under test
<template>
<div>
<button class="yes" @click="callYes">Yes</button>
<button class="no" @click="callNo">No</button>
</div>
</template>
<script>
export default {
name: 'YesNoComponent',
props: {
callMe: {
type: Function
}
},
methods: {
callYes() {
this.callMe('yes')
},
callNo() {
this.callMe('no')
}
}
}
</script>
Test
import YesNoComponent from '@/components/YesNoComponent'
import { mount } from '@vue/test-utils'
import sinon from 'sinon'
it('Click on yes button calls our method with argument "yes"', async () => {
const spy = sinon.spy()
const wrapper = mount(YesNoComponent, {
propsData: {
callMe: spy
}
})
await wrapper.find('button.yes').trigger('click')
spy.should.have.been.calledWith('yes')
})
Keyboard Example
Component under test
This component allows to increment/decrement the quantity using various keys.
<template>
<input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>
<script>
const KEY_DOWN = 40
const KEY_UP = 38
const ESCAPE = 27
export default {
data() {
return {
quantity: 0
}
},
methods: {
increment() {
this.quantity += 1
},
decrement() {
this.quantity -= 1
},
clear() {
this.quantity = 0
},
onKeydown(e) {
if (e.keyCode === ESCAPE) {
this.clear()
}
if (e.keyCode === KEY_DOWN) {
this.decrement()
}
if (e.keyCode === KEY_UP) {
this.increment()
}
if (e.key === 'a') {
this.quantity = 13
}
}
},
watch: {
quantity: function (newValue) {
this.$emit('input', newValue)
}
}
}
</script>
Test
import QuantityComponent from '@/components/QuantityComponent'
import { mount } from '@vue/test-utils'
describe('Key event tests', () => {
it('Quantity is zero by default', () => {
const wrapper = mount(QuantityComponent)
expect(wrapper.vm.quantity).toBe(0)
})
it('Up arrow key increments quantity by 1', async () => {
const wrapper = mount(QuantityComponent)
await wrapper.trigger('keydown.up')
expect(wrapper.vm.quantity).toBe(1)
})
it('Down arrow key decrements quantity by 1', async () => {
const wrapper = mount(QuantityComponent)
wrapper.vm.quantity = 5
await wrapper.trigger('keydown.down')
expect(wrapper.vm.quantity).toBe(4)
})
it('Escape sets quantity to 0', async () => {
const wrapper = mount(QuantityComponent)
wrapper.vm.quantity = 5
await wrapper.trigger('keydown.esc')
expect(wrapper.vm.quantity).toBe(0)
})
it('Magic character "a" sets quantity to 13', async () => {
const wrapper = mount(QuantityComponent)
await wrapper.trigger('keydown', {
key: 'a'
})
expect(wrapper.vm.quantity).toBe(13)
})
})
Limitations
A key name after the dot keydown.up
is translated to a keyCode
. This is supported for the following names:
key name | key code |
---|---|
enter | 13 |
esc | 27 |
tab | 9 |
space | 32 |
delete | 46 |
backspace | 8 |
insert | 45 |
up | 38 |
down | 40 |
left | 37 |
right | 39 |
end | 35 |
home | 36 |
pageup | 33 |
pagedown | 34 |
Testing Asynchronous Behavior
There are two types of asynchronous behavior you will encounter in your tests:
- Updates applied by Vue
- Asynchronous behavior outside of Vue
Updates applied by Vue
Vue batches pending DOM updates and applies them asynchronously to prevent unnecessary re-renders caused by multiple data mutations.
You can read more about asynchronous updates in the Vue docs
In practice, this means that after mutating a reactive property, to assert that change your test has to wait while Vue is performing updates.
One way is to use await Vue.nextTick()
, but an easier and cleaner way is to just await
the method that you mutated the state with, like trigger
.
// inside test-suite, add this test case
it('button click should increment the count text', async () => {
expect(wrapper.text()).toContain('0')
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.text()).toContain('1')
})
Awaiting the trigger above is the same as doing:
it('button click should increment the count text', async () => {
expect(wrapper.text()).toContain('0')
const button = wrapper.find('button')
button.trigger('click')
await Vue.nextTick()
expect(wrapper.text()).toContain('1')
})
Methods that can be awaited are:
Asynchronous behavior outside of Vue
One of the most common asynchronous behaviors outside of Vue is API calls in Vuex actions. The following examples shows how to test a method that makes an API call. This example uses Jest to run the test and to mock the HTTP library axios
. More about Jest manual mocks can be found here.
The implementation of the axios
mock looks like this:
export default {
get: () => Promise.resolve({ data: 'value' })
}
The below component makes an API call when a button is clicked, then assigns the response to value
.
<template>
<button @click="fetchResults">{{ value }}</button>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
value: null
}
},
methods: {
async fetchResults() {
const response = await axios.get('mock/service')
this.value = response.data
}
}
}
</script>
A test can be written like this:
import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios', () => ({
get: Promise.resolve('value')
}))
it('fetches async when a button is clicked', () => {
const wrapper = shallowMount(Foo)
wrapper.find('button').trigger('click')
expect(wrapper.text()).toBe('value')
})
This test currently fails because the assertion is called before the promise in fetchResults
resolves. Most unit test libraries provide a callback to let the runner know when the test is complete. Jest and Mocha both use done
. We can use done
in combination with $nextTick
or setTimeout
to ensure any promises are settled before the assertion is made.
it('fetches async when a button is clicked', done => {
const wrapper = shallowMount(Foo)
wrapper.find('button').trigger('click')
wrapper.vm.$nextTick(() => {
expect(wrapper.text()).toBe('value')
done()
})
})
The reason setTimeout
allows the test to pass is because the microtask queue where promise callbacks are processed runs before the task queue, where setTimeout
callbacks are processed. This means by the time the setTimeout
callback runs, any promise callbacks on the microtask queue will have been executed. $nextTick
on the other hand schedules a microtask, but since the microtask queue is processed first-in-first-out that also guarantees the promise callback has been executed by the time the assertion is made. See here for a more detailed explanation.
Another solution is to use an async
function and a package like flush-promises. flush-promises
flushes all pending resolved promise handlers. You can await
the call of flushPromises
to flush pending promises and improve the readability of your test.
The updated test looks like this:
import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')
it('fetches async when a button is clicked', async () => {
const wrapper = shallowMount(Foo)
wrapper.find('button').trigger('click')
await flushPromises()
expect(wrapper.text()).toBe('value')
})
This same technique can be applied to Vuex actions, which return a promise by default.
await button.trigger()
?
Why not just As explained above, there is a difference between the time it takes for Vue to update its components,
and the time it takes for a Promise, like the one from axios
to resolve.
A nice rule to follow is to always await
on mutations like trigger
or setProps
.
If your code relies on something async, like calling axios
, add an await to the flushPromises
call as well.
Using with TypeScript
An example project for this setup is available on GitHub.
TypeScript is a popular superset of JavaScript that adds types and classes on top of regular JS. Vue Test Utils includes types in the distributed package, so it works well with TypeScript.
In this guide, we'll walk through how to setup a testing setup for a TypeScript project using Jest and Vue Test Utils from a basic Vue CLI TypeScript setup.
Adding TypeScript
First you need to create a project. If you don't have Vue CLI installed, install it globally:
$ npm install -g @vue/cli
And create a project by running:
$ vue create hello-world
In the CLI prompt, choose to Manually select features
, select TypeScript, and press enter. This will create a project with TypeScript already configured.
NOTE
If you want a more detailed guide on setting up Vue with TypeScript, checkout the TypeScript Vue starter guide.
The next step is to add Jest to the project.
Setting up Jest
Jest is a test runner developed by Facebook, aiming to deliver a battery-included unit testing solution. You can learn more about Jest on its official documentation.
Install Jest and Vue Test Utils:
$ npm install --save-dev jest @vue/test-utils
Next define a test:unit
script in package.json
.
// package.json
{
// ..
"scripts": {
// ..
"test:unit": "jest"
}
// ..
}
Processing Single-File Components in Jest
To teach Jest how to process *.vue
files, we need to install and configure the vue-jest
preprocessor:
npm install --save-dev vue-jest
Next, create a jest
block in package.json
:
{
// ...
"jest": {
"moduleFileExtensions": [
"js",
"ts",
"json",
// tell Jest to handle `*.vue` files
"vue"
],
"transform": {
// process `*.vue` files with `vue-jest`
".*\\.(vue)$": "vue-jest"
},
"testURL": "http://localhost/"
}
}
Configuring TypeScript for Jest
In order to use TypeScript files in tests, we need to set up Jest to compile TypeScript. For that we need to install ts-jest
:
$ npm install --save-dev ts-jest
Next, we need to tell Jest to process TypeScript test files with ts-jest
by adding an entry under jest.transform
in package.json
:
{
// ...
"jest": {
// ...
"transform": {
// ...
// process `*.ts` files with `ts-jest`
"^.+\\.tsx?$": "ts-jest"
}
// ...
}
}
Placing Test Files
By default, Jest will recursively pick up all files that have a .spec.js
or .test.js
extension in the entire project.
To run test files with a .ts
extension, we need to change the testRegex
in the config section in the package.json
file.
Add the following to the jest
field in package.json
:
{
// ...
"jest": {
// ...
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
}
}
Jest recommends creating a __tests__
directory right next to the code being tested, but feel free to structure your tests as you see fit. Just beware that Jest would create a __snapshots__
directory next to test files that performs snapshot testing.
Writing a unit test
Now we've got the project set up, it's time to write a unit test.
Create a src/components/__tests__/HelloWorld.spec.ts
file, and add the following code:
// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld.vue', () => {
test('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})
That's all we need to do to get TypeScript and Vue Test Utils working together!
Resources
Using with Vue Router
Installing Vue Router in tests
You should never install Vue Router on the Vue base constructor in tests. Installing Vue Router adds $route
and $router
as read-only properties on Vue prototype.
To avoid this, we can create a localVue, and install Vue Router on that.
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()
shallowMount(Component, {
localVue,
router
})
Note: Installing Vue Router on a
localVue
also adds$route
and$router
as read-only properties to alocalVue
. This means you can not use themocks
option to overwrite$route
and$router
when mounting a component using alocalVue
with Vue Router installed.
router-link
or router-view
Testing components that use When you install Vue Router, the router-link
and router-view
components are registered. This means we can use them anywhere in our application without needing to import them.
When we run tests, we need to make these Vue Router components available to the component we're mounting. There are two methods to do this.
Using stubs
import { shallowMount } from '@vue/test-utils'
shallowMount(Component, {
stubs: ['router-link', 'router-view']
})
Installing Vue Router with localVue
import { mount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
const localVue = createLocalVue()
localVue.use(VueRouter)
mount(Component, {
localVue,
router
})
The router instance is available to all children components, this is useful for integration level testing.
$route
and $router
Mocking Sometimes you want to test that a component does something with parameters from the $route
and $router
objects. To do that, you can pass custom mocks to the Vue instance.
import { shallowMount } from '@vue/test-utils'
const $route = {
path: '/some/path'
}
const wrapper = shallowMount(Component, {
mocks: {
$route
}
})
wrapper.vm.$route.path // /some/path
Note: the mocked
$route
and$router
values are not available to children components, either stub this components or use thelocalVue
method.
Common gotchas
Installing Vue Router adds $route
and $router
as read-only properties on Vue prototype.
This means any future tests that try to mock $route
or $router
will fail.
To avoid this, never install Vue Router globally when you're running tests; use a localVue
as detailed above.
Using with Vuex
In this guide, we'll see how to test Vuex in components with Vue Test Utils, and how to approach testing a Vuex store.
Testing Vuex in components
Mocking Actions
Let’s look at some code.
This is the component we want to test. It calls Vuex actions.
<template>
<div class="text-align-center">
<input type="text" @input="actionInputIfTrue" />
<button @click="actionClick()">Click</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions(['actionClick']),
actionInputIfTrue: function actionInputIfTrue(event) {
const inputValue = event.target.value
if (inputValue === 'input') {
this.$store.dispatch('actionInput', { inputValue })
}
}
}
}
</script>
For the purposes of this test, we don’t care what the actions do, or what the store looks like. We just need to know that these actions are being fired when they should, and that they are fired with the expected value.
To test this, we need to pass a mock store to Vue when we shallowMount our component.
Instead of passing the store to the base Vue constructor, we can pass it to a - localVue. A localVue is a scoped Vue constructor that we can make changes to without affecting the global Vue constructor.
Let’s see what this looks like:
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Actions.vue', () => {
let actions
let store
beforeEach(() => {
actions = {
actionClick: jest.fn(),
actionInput: jest.fn()
}
store = new Vuex.Store({
actions
})
})
it('dispatches "actionInput" when input event value is "input"', () => {
const wrapper = shallowMount(Actions, { store, localVue })
const input = wrapper.find('input')
input.element.value = 'input'
input.trigger('input')
expect(actions.actionInput).toHaveBeenCalled()
})
it('does not dispatch "actionInput" when event value is not "input"', () => {
const wrapper = shallowMount(Actions, { store, localVue })
const input = wrapper.find('input')
input.element.value = 'not input'
input.trigger('input')
expect(actions.actionInput).not.toHaveBeenCalled()
})
it('calls store action "actionClick" when button is clicked', () => {
const wrapper = shallowMount(Actions, { store, localVue })
wrapper.find('button').trigger('click')
expect(actions.actionClick).toHaveBeenCalled()
})
})
What’s happening here? First we tell Vue to use Vuex with the localVue.use
method. This is just a wrapper around Vue.use
.
We then make a mock store by calling new Vuex.Store
with our mock values. We only pass it the actions, since that’s all we care about.
The actions are jest mock functions. These mock functions give us methods to assert whether the actions were called or not.
We can then assert in our tests that the action stub was called when expected.
Now the way we define the store might look a bit foreign to you.
We’re using beforeEach
to ensure we have a clean store before each test. beforeEach
is a mocha hook that’s called before each test. In our test, we are reassigning the store variables value. If we didn’t do this, the mock functions would need to be automatically reset. It also lets us change the state in our tests, without it affecting later tests.
The most important thing to note in this test is that we create a mock Vuex store and then pass it to Vue Test Utils.
Great, so now we can mock actions, let’s look at mocking getters.
Mocking Getters
<template>
<div>
<p v-if="inputValue">{{inputValue}}</p>
<p v-if="clicks">{{clicks}}</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: mapGetters(['clicks', 'inputValue'])
}
</script>
This is a fairly simple component. It renders the result of the getters clicks
and inputValue
. Again, we don’t really care about what those getters return – just that their result is being rendered correctly.
Let’s see the test:
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Getters from '../../../src/components/Getters'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Getters.vue', () => {
let getters
let store
beforeEach(() => {
getters = {
clicks: () => 2,
inputValue: () => 'input'
}
store = new Vuex.Store({
getters
})
})
it('Renders "store.getters.inputValue" in first p tag', () => {
const wrapper = shallowMount(Getters, { store, localVue })
const p = wrapper.find('p')
expect(p.text()).toBe(getters.inputValue())
})
it('Renders "store.getters.clicks" in second p tag', () => {
const wrapper = shallowMount(Getters, { store, localVue })
const p = wrapper.findAll('p').at(1)
expect(p.text()).toBe(getters.clicks().toString())
})
})
This test is similar to our actions test. We create a mock store before each test, pass it as an option when we call shallowMount
, and assert that the value returned by our mock getters is being rendered.
This is great, but what if we want to check our getters are returning the correct part of our state?
Mocking with Modules
Modules are useful for separating out our store into manageable chunks. They also export getters. We can use these in our tests.
Let’s look at our component:
<template>
<div>
<button @click="moduleActionClick()">Click</button>
<p>{{moduleClicks}}</p>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
methods: {
...mapActions(['moduleActionClick'])
},
computed: mapGetters(['moduleClicks'])
}
</script>
Simple component that includes one action and one getter.
And the test:
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '../../../src/components/MyComponent'
import myModule from '../../../src/store/myModule'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('MyComponent.vue', () => {
let actions
let state
let store
beforeEach(() => {
state = {
clicks: 2
}
actions = {
moduleActionClick: jest.fn()
}
store = new Vuex.Store({
modules: {
myModule: {
state,
actions,
getters: myModule.getters,
namespaced: true
}
}
})
})
it('calls store action "moduleActionClick" when button is clicked', () => {
const wrapper = shallowMount(MyComponent, { store, localVue })
const button = wrapper.find('button')
button.trigger('click')
expect(actions.moduleActionClick).toHaveBeenCalled()
})
it('renders "state.clicks" in first p tag', () => {
const wrapper = shallowMount(MyComponent, { store, localVue })
const p = wrapper.find('p')
expect(p.text()).toBe(state.clicks.toString())
})
})
Testing a Vuex Store
There are two approaches to testing a Vuex store. The first approach is to unit test the getters, mutations, and actions separately. The second approach is to create a store and test against that. We'll look at both approaches.
To see how to test a Vuex store, we're going to create a simple counter store. The store will have an increment
mutation and an evenOrOdd
getter.
// mutations.js
export default {
increment(state) {
state.count++
}
}
// getters.js
export default {
evenOrOdd: state => (state.count % 2 === 0 ? 'even' : 'odd')
}
Testing getters, mutations, and actions separately
Getters, mutations, and actions are all JavaScript functions, so we can test them without using Vue Test Utils and Vuex.
The benefit to testing getters, mutations, and actions separately is that your unit tests are detailed. When they fail, you know exactly what is wrong with your code. The downside is that you will need to mock Vuex functions, like commit
and dispatch
. This can lead to a situation where your unit tests pass, but your production code fails because your mocks are incorrect.
We'll create two test files, mutations.spec.js
and getters.spec.js
:
First, let's test the increment
mutations:
// mutations.spec.js
import mutations from './mutations'
test('"increment" increments "state.count" by 1', () => {
const state = {
count: 0
}
mutations.increment(state)
expect(state.count).toBe(1)
})
Now let's test the evenOrOdd
getter. We can test it by creating a mock state
, calling the getter with the state
and checking it returns the correct value.
// getters.spec.js
import getters from './getters'
test('"evenOrOdd" returns even if "state.count" is even', () => {
const state = {
count: 2
}
expect(getters.evenOrOdd(state)).toBe('even')
})
test('"evenOrOdd" returns odd if "state.count" is odd', () => {
const state = {
count: 1
}
expect(getters.evenOrOdd(state)).toBe('odd')
})
Testing a running store
Another approach to testing a Vuex store is to create a running store using the store config.
The benefit of creating a running store instance is we don't have to mock any Vuex functions.
The downside is that when a test breaks, it can be difficult to find where the problem is.
Let's write a test. When we create a store, we'll use localVue
to avoid polluting the Vue base constructor. The test creates a store using the store-config.js
export:
// store-config.js
import mutations from './mutations'
import getters from './getters'
export default {
state: {
count: 0
},
mutations,
getters
}
// store-config.spec.js
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'
test('increments "count" value when "increment" is committed', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store(cloneDeep(storeConfig))
expect(store.state.count).toBe(0)
store.commit('increment')
expect(store.state.count).toBe(1)
})
test('updates "evenOrOdd" getter when "increment" is committed', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store(cloneDeep(storeConfig))
expect(store.getters.evenOrOdd).toBe('even')
store.commit('increment')
expect(store.getters.evenOrOdd).toBe('odd')
})
Notice that we use cloneDeep
to clone the store config before creating a store with it. This is because Vuex mutates the options object used to create the store. To make sure we have a clean store in each test, we need to clone the storeConfig
object.
However, cloneDeep
is not "deep" enough to also clone store modules. If your storeConfig
includes modules, you need to pass an object to new Vuex.Store()
, like so:
import myModule from './myModule'
// ...
const store = new Vuex.Store({ modules: { myModule: cloneDeep(myModule) } })
Resources
← Installation API →