Structuring and testing a Vue/Vuex app with vue-test-utils
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.
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.js
inside 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:
- We import
vue
,vuex
, and doVue.use(Vuex
. Now we can use vuex in the test. - Before each test, we mock what the store will look like using
beforeEach
. This will run before eachit
block. - We render the
TodoContainer
usingshallow
. Any components, likeTodoItem
used in the component we are rendering usingshallow
can be stubbed using thestubs
object. - 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 commit
with a TOGGLE_COMPLETE
mutation handler. Here are two common ways to implement this:
- Inside
TodoItem
, have aclick
event on the outer<div>
. Handle the commit there. - In
TodoContainer
, have aclick
event on the actual<TodoItem>
. This is what I will go with — I don’t wantTodoItem
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.
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.