Monday, 20 March, 2017 UTC


Summary

A few months months ago I’ve started working on a project that required me to write a lot of JavaScript. As I was writing more and more code it became apparent that the lack of unit tests was slowing me down. There wasn’t a way for me to verify if anything broke with my latest feature and it became frustrating going over everything manually.
I’ve realized testing was my highest priority from that point so I started reading and learning. Some of the choices came naturally. Karma, Mocha, Chai and Sinon.JS are great bundles and they offer everything you might need.
There was still a problem when it came to testing components that interact with APIs, Vuex and sockets. I had a specific problem with testing interactions between Vue components and a Vuex store. So I’ve decided to write a simple example of what might work for someone in the same situation.
Starting Point
I will be using vue-cli to set up Vue for this post. It scaffolds your project and, most importantly, configures the entire testing environment for you. Vue-cli will prompt you about what kind of setup you want. For the purposes of this post, I did not want to set up end-to-end testing with NightWatch so you can skip that now, but it’s definitely something to look into:
$ npm install -g vue-cli $ vue init webpack my-project $ cd my-project $ npm install 
After this you can run the project and see if everything is working with:
$ npm run dev 
I would also suggest you run the tests and verify everything is working as expected with:
$ npm test run 
Installing Vuex
From official docs: “Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.”
Say you have a few Vue components on the same page and they need to modify the same data, e.g. you have a list of tasks, modal window for creating a new task and for modifying existing tasks, and a sidebar that displays the last three modified tasks.
These components obviously need to communicate between each other and there is a way to accomplishing this without Vuex, like emitting events from child to parent and passing props to children or defining data on the Vue app instance, but both of these come with some disadvantages.
This is where Vuex comes in and provides an easy and maintainable solution to these types of problems.
In order to install it, navigate to the root of your project and run:
$ npm install --save vuex 
Since I’ll be using the default vue-cli Karma setup which uses PhantomJS we need to use the Promise polyfill for Vuex to work:
$ npm install --save-dev es6-promise 
Ok, with that out of the way let’s have some fun and include Vuex in our project. In the src/ folder add a store folder with the following directory structure:
  • index.js ( bootstrapping our store )
  • mutation-types.js
  • actions.js
  • getters.js
  • api.js ( api service )
  • items.json ( our imaginary API )
Vuex can be set up in just one file, but it becomes a mess if you are handling a lot of mutations and it becomes more difficult to test. If you’re not familiar with Vuex you might want to read through the official docs first.
src/store/index.js
require('es6-promise').polyfill() import Vue from 'vue/dist/vue.common.js' import Vuex from 'vuex/dist/vuex.js' Vue.use(Vuex) import * as types from './mutation-types' import * as actions from './actions' import * as getters from './getters' const state = { items: [] } const mutations = { [types.setItems] (state, items) { state.items = items } } const options = { state, mutations, actions, getters } export default new Vuex.Store(options) export { options } 
src/store/actions.js
import * as types from './mutation-types' import api from './api' export const setItems = ({commit}) => { api.getItems() .then((items) => { commit(types.setItems, items) }) .catch((err) => console.log(err)) } 
src/store/mutation-types.js
export const setItems = 'SET_ITEMS' 
src/store/getters.js
export const items = state => { return state.items } 
src/store/api.js
export default { getItems: function () { return new Promise((resolve, reject) => { // imagine we're making an API request here const response = require('./items.json') resolve(response.body) }) } } 
Server Response: src/store/items.json
{ "body": [ "coffee", "sugar", "water" ] } 
Now that we have our basic Vuex store setup we need to include it in our Vue app instance:
src/main.js
... import store from './store' ... new Vue({ el: '#app', store, // uses our newly created store in our Vue instance template: '<App/>', components: { App } }) 
Vue Components that Depend on Vuex State
Now that we’ve setup our Vuex store and included it in our Vue app instance let’s make a very, very simple Vue component that uses the store state. Create a new file src/components/Items.vue with the following content:
<template> <ul> <li v-for="(item, index) in items" :key="index" class="items"> {{ item }} </li> </ul> </template> <script> import { mapActions, mapGetters } from 'vuex' export default { mounted () { this.setItems() }, methods: { ...mapActions(['setItems']) }, computed: { ...mapGetters(['items']) } } </script> 
As you can see, there is nothing interesting to see here. We’re rendering a list of items from our store. In the mounted hook we dispatch an action that fetches data from the API and sets it in the state. MapActions and mapGetters are just a convenient way to inject actions and getters directly into our component instead of doing this.$store.dispatch(‘setItems’) or this.$store.state.items.
Writing Unit Tests for the Items.vue Component
So how to go about testing it? Obviously, this component can’t be tested without being able to access the store. So we need to include the store in our tests, but what about our API? We could test it as is but there are too many variables there and the testing environment must be sterile and stay the same every time.
Let’s take it step by step. Create a new file test/unit/specs/Items.spec.js with the following content:
require('es6-promise').polyfill() import Vue from 'vue/dist/vue.common.js' import Vuex from 'vuex/dist/vuex.js' import store from '../../../src/store' import Items from '../../../src/components/Items.vue' describe('Items.vue', () => { it('test initial rendering with api', (done) => { const vm = new Vue({ template: '<div><test></test></div>', store, components: { 'test': Items } }).$mount() Vue.nextTick() .then(() => { expect(vm.$el.querySelectorAll('.items').length).to.equal(3) done() }) .catch(done) }) }) 
In this test we are mounting a new Vue app instance with only our component included in its template as <test></test>. After the app is mounted so is the component, and in its mounted hook we dispatch the Vuex action and subsequent API request. We use Vue.nextTick() because DOM updates are async and if we didn’t use it our test would not pass because the DOM would not be updated at that time. This test will pass because our API is just reading from a file, but when we add another array item to src/store/items.json it will fail.
So what we need to do is mock up the src/store/api.js service. For this we’re going to use inject-loader. So the first thing we need to do is install it:
$ npm install [email protected]^2.0.0 
Now that we have that installed, we can rewrite our test and inject our mock API service into src/store/actions.js. Let’s modify our test/unit/specs/Items.spec.js file to contain the following:
require('es6-promise').polyfill() import Vue from 'vue/dist/vue.common.js' import Vuex from 'vuex/dist/vuex.js' import Items from '../../../src/components/Items.vue' import * as types from '../../../src/store/mutation-types' import * as getters from '../../../src/store/getters' describe('Items.vue', () => { it('test initial rendering with mock data', (done) => { const actionsInjector = require('inject-loader!../../../src/store/actions') const actions = actionsInjector({ './api': { getItems () { return new Promise((resolve, reject) => { const arr = ['Cat', 'Dog', 'Fish', 'Snail'] resolve(arr) }) } } }) const state = { items: [] } const mutations = { [types.setItems] (state, items) { state.items = items } } const options = { state, mutations, actions, getters } const mockStore = new Vuex.Store(options) const vm = new Vue({ template: '<div><test></test></div>', store: mockStore, components: { 'test': Items } }).$mount() Vue.nextTick() .then(() => { expect(vm.$el.querySelectorAll('.items').length).to.equal(4) done() }) .catch(done) }) }) 
The strange inline require in actions.js basically injects our real actions.js file but allows us to stub the dependencies of that file. The next line in that file demonstrates how that’s done with actionsInjector. We replace our API object with our own that returns consistent data across tests. That’s it, now all we have to do is run:
$ npm test run 
And enjoy all the green lines! 🎉🍕