Structuring and testing a Vue/Vuex app with vue-test-utils

Lachlan Miller
8 min readOct 21, 2017

--

With vue-test-utils hitting the scene and making testing a breeze, I’ve been exploring the best way to structure my Vue apps, and how to test large applications driven primary by a complex Vuex store.

In this post I’ll build… a todo app. To illustrate the various things vue-test-utils provides, and how to test Vuex, I’ll make heavy use of many different parts of the Vue and Vuex API. I’ll coverage as much code as possible. Jest will be our test runner.

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

App Structure

The structure is inspired by this post about structuring React apps. Basically, each component has a folder, with the styles, and index.vue and a index.test . You can also have most nested components.

I will the following structure:

/src
/App.vue
/components
/TodoContainer
/index.vue
/index.test.js
/components
/TodoItem
/index.vue
/index.test.js
/NewTodoForm
/index.vue
/index.test.js
/TodoFilter
/index.vue
/index.test.js
/store
/index.js
/index.test.js

To get started, scaffold a new project and install some extra deps:

vue init webpack-simple vue-test-utils-democd vue-test-utils-demonpm install vue-test-utils jest-vue-preprocessor babel-jest vuex jest

Now in package.json, add the following to allow Jest to work with .vue files. Also add a script, "test": "jest"

"jest": {
"moduleFileExtensions": [
"js",
"json",
"vue"
],
"transform": {
".*\\.(vue)$": "<rootDir>/node_modules/jest-vue-preprocessor",
"^.+\\.js$": "<rootDir>/node_modules/babel-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"mapCoverage": true
}

Lastly, update .babelrc :

{   
"presets": [
["env", { "modules": false }]
],
"env": {
"test": {
"presets": [
["env", { "targets": { "node": "current" }}]
]
}
}
}

Okay. To make sure it’s all working, create App.test.jsinside of src. Update App.vue to look like this:

<template>
<div id="app">

</div>
</template>
<script>
export default {
name: 'app',
}
</script>

Inside app App.test.js :

import { shallow } from 'vue-test-utils'
import App from './App'
describe('App', () => {
it('works', () => {
const wrapper = shallow(App)
})
})

Run npm test and you should get:

PASS  src/App.test.js
App
✓ works (9ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.197s

Okay, time to develop. Create the folder structure outlined above.

Testing TodoItem

The first component we will use TDD to develop is TodoItem.vue. TodoItem receives a todo as a prop, and depending on the complete status, applies the correct class. We will mock the todo — we don’t really care how or where it comes from, just that is is passed to the TodoItem.vue

import {shallow} from 'vue-test-utils'
import TodoItem from './'
describe('TodoItem', () => {
it('renders a todo item', () => {
const wrapper = shallow(TodoItem, {
propsData: {
todo: {
text: 'Do work',
completed: false
}
}
})
expect(wrapper.html().includes('Do work')).toBe(true)
})
})

This is easy enough to implement, let’s get to it:

<template>
<div class="todo">
{{ todo.text }}
</div>
</template>
<script>
export default {
name: 'index',
props: {
todo: {
required: true,
type: Object
}
}
}
</script>
<style scoped>
</style>

And we have a passing test! How about an completed todo? I want to apply a class completed, and assign some styling.

TodoItem — Failing Test

it('assigns `completed` class to a completed todo', () => {
const wrapper = shallow(TodoItem, {
propsData: {
todo: {
text: 'Do work',
complete: true
}
}
})
expect(wrapper.hasClass('completed')).toBe(true)
})

To get this to pass, we can bind to the class of the todo, and apply text-decoration: line-through.

TodoItem — Passing Test

<template>
<div :class="[todo.complete ? 'completed' : 'todo']">
{{ todo.text }}
</div>
</template>
<script>
/* */
</script>
<style scoped>
.completed {
text-decoration: line-through;
}
</style>

Looks good! TodoItem doesn’t have any methods, it just relies on props, so there isn’t anything else left to test. This is also known as a “dumb” or “presentation” component. Let’s move on to TodoContainer, a which has a bit more logic.

Before writing the test, let’s discuss how the implementation will work:

TodoContainer will have access to the $store. $store.state will contain a todos object, and an ids array. Read more about this concept here, but basically it can be beneficial to set your store up like this, as opposed to a single array of todo objects.

TodoContainer will loop over ids, and for each one, render a TodoItem. That’s enough to get us started.

TodoContainer — Failing Test

import Vue from 'vue'
import Vuex from 'vuex'
import {shallow} from 'vue-test-utils'
import TodoContainer from './'
Vue.use(Vuex)describe('TodoContainer', () => {
let store
beforeEach(() => {
store = new Vuex.Store({
state: {
todos: {
'0': {
text: 'Do some work',
completed: false
},
'1': {
text: 'Take a rest',
completed: false
}
},
ids: [0, 1]
}
})
})
it('renders two todo items', () => {
const wrapper = shallow(TodoContainer, {
store,
stubs: {
TodoItem: '<TodoItem />'
}
})
const todos = wrapper.findAll('TodoItem') expect(todos.length).toBe(2)
})
})

A lot going on here. Let’s break it down:

  1. We import vue , vuex, and do Vue.use(Vuex . Now we can use vuex in the test.
  2. Before each test, we mock what the store will look like using beforeEach. This will run before each it block.
  3. We render the TodoContainer using shallow . Any components, like TodoItem used in the component we are rendering using shallow can be stubbed using the stubs object.
  4. Lastly, we just find all TodoItems. We expect there is two.

A bit of work to set this up. You can write a utility to do the Vuex setup if you like, and maybe even create some fake data to reuse. I like to declare the state for each test, so I can tailor it to different use cases.

TodoContainer — Passing Test

<template>
<div>
<TodoItem
v-for="id in $store.state.ids"
:key="id"
:todo="$store.state.todos[id]"
/>
</div>
</template>
<script>
import TodoItem from './components/TodoItem'
export default {
name: 'index',
components: {
TodoItem
}
}
</script>
<style scoped>
</style>

Not that much to explain. Just looping the ids and passing a todo prop to each TodoItem.

Testing mutation commits

This part is more interesting. When I click on a TodoItem, I want to toggle it’s complete property by calling commitwith a TOGGLE_COMPLETE mutation handler. Here are two common ways to implement this:

  1. Inside TodoItem , have a click event on the outer <div>. Handle the commit there.
  2. In TodoContainer , have a click event on the actual <TodoItem> . This is what I will go with — I don’t want TodoItem to be very smart.

First, let’s declare the mutations after the store inside of beforeEach , and mock the mutation using jest.fn() .

describe('TodoContainer', () => {
let store
let mutations
beforeEach(() => {
mutations = {
TOGGLE_COMPLETE: jest.fn()
}
store = new Vuex.Store({
state: {
todos: { /* */ }
ids: [ /* */ ]
},
mutations
})
})

We don’t actually want to execute the mutation logic — all we care is that it is committed. The logic will be tested later in the store tests.

Now, a new it block for the new test.

commit(TOGGLE_COMPLETE) — Failing Test

it('commits a TOGGLE_COMPLETE mutation when clicked', () => {
const wrapper = shallow(TodoContainer, {
store,
stubs: {
TodoItem: '<TodoItem />'
}
})
const todo = wrapper.find('TodoItem')
todo.trigger('click')
expect(mutations.TOGGLE_COMPLETE.mock.calls[0][1])
.toHaveProperty('id')
expect(mutations.TOGGLE_COMPLETE.mock.calls.length).toBe(1)
})

You can simulate events using trigger , then assert the mutation was committed. The first expectation is asserting the second argument — the payload for the mutation, contains id. The first argument is for a mutation is always the state. We want to pass the id so the store knows which todo to complete.

commit(TOGGLE_COMPLETE) — Passing Test

Now, the passing implementation:

<template>
<div>
<TodoItem
v-for="id in $store.state.ids"
:key="id"
:todo="$store.state.todos[id]"
@click.native="complete(id)"
/>
</div>
</template>
<script>
import TodoItem from './components/TodoItem'
export default {
name: 'index',
components: {
TodoItem
},
methods: {
complete (id) {
this.$store.commit('TOGGLE_COMPLETE', { id })
}
}
}
</script>

Note you need to use .native — because we are handling the even on the actual component, not the internal html rendered by TodoItem.

Testing the store

Testing the store is very easy, because it’s just plain old JavaScript functions, nothing to do with Vue or Vuex. Let’s see how to do it, anyway. Head over to store/index.test.js .

TOGGLE_COMPLETE — Failing Test

import { mutations } from './'describe('mutations', () => {
it('completes a incomplete todo', () => {
let state = {
todos: {
'0': { complete: false }
}
}
mutations.TOGGLE_COMPLETE(state, { id: 0 }) expect(state.todos[0].complete).toBe(true)
})
})

We just need to call the mutation, and assert the complete property is changed.

TOGGLE_COMPLETE — Passing Test

Let’s set the store up really quickly and make this test pass.

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)const state = {
todos: {},
ids: []
}
export const mutations = {
TOGGLE_COMPLETE (state, { id }) {
state.todos[id].complete = ! state.todos[id].complete
}
}
export default new Vuex.Store({
state,
mutations
})

Easy. Note we have to export const mutations to be able to use them in the test. Nothing too exciting here. Note we do NOT need to import or create a store — we don’t need to test whether Vuex commit function works or not, we know it does because it has it’s own internal tests. Remember, test your code, not the framework.

Now we just need a way to add todos.

Testing the NewTodoForm

NewTodoForm is similar to TodoContainer , we wil just be asserting the correct method are called when we interact with it.

NewTodoForm — Failing Test

import {shallow} from 'vue-test-utils'
import NewTodoForm from './'
describe('NewTodoForm', () => {
it('calls createTodo when enter is pressed', () => {
const createTodo = jest.fn()
const wrapper = shallow(NewTodoForm)
wrapper.setMethods({ createTodo })
const input = wrapper.find('input')
input.trigger('keydown.enter')
expect(createTodo.mock.calls.length).toBe(1)
})
})

Instead of mocking a mutation, I want to show how to mock Vue instance methods, using setMethods. The actual implementation, as you will see, will call createTodo, which in turn commits a mutation — we already saw how to test that above though, so I wanted to show some of other things vue-test-utils can do.

NewTodoForm — Passing Test

<template>
<div>
<input v-model="newTodoText" @keydown.enter="createTodo"/>
</div>
</template>
<script>
export default {
name: 'index',
data () {
return {
newTodoText: ''
}
},
methods: {
createTodo () {
this.$store.commit('CREATE_TODO', {
text: this.newTodoText
})
this.newTodoText = ''
}
}
}
</script>

Another fairly straight forward component. Not much to explain — we can use keydown.enter , exactly like we wrote in the test, to call a method when enter is pressed.

CREATE_TODO — Failing Test

Let’s see how to test CREATE_TODO — again, just plain JavaScript, not nothing flashy.

it('creates a todo by calling CREATE_TODO', () => {
let state = {
todos: {
},
ids: []
}
mutations.CREATE_TODO(state, { text: 'New todo' }) expect(state.todos[1]).toHaveProperty('text')
expect(state.todos[1].text).toEqual('New todo')
expect(state.ids.length).toBe(1)
})

Just assert the todo was added, with the text. The implementation equally straight forward:

CREATE_TODO (state, { text }) {
let id = state.ids.length + 1
state.todos = Object.assign({},
state.todos, {
[id]: { text, complete: false }
}
)
state.ids.push(id)
}

Not the most robust way to choose an id, but it’s fine for an example. You can use the spread operator ... instead of Object.assign to add the new todo, but the webpack-simple template doesn’t include it, so I used Object.assign .

With that, everything is working — we haven’t actually set up the App, imported the store or rendered anything, but that’s easy enough to do.

Let’s check the coverage using jest’s --coverage flag.

Jest ` — coverage` flag

Everything is at 100% except NewTodoForm — the data() function is not tested. All we are doing is binding using v-model anyway, so it’s fine. We don’t need to test that v-model works — test your code, the framework has it’s own tests, so we can be confident that v-model does indeed work.

I’m still figuring out the best application structure, but having the tests next to the components feels much easier to navigate then a separate folder. Any ideas of alternatives are more than welcome, or suggestions for future articles about testing.

--

--

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.