Tutorial

Testing Vuex-Dependent Vue.js Components

Published on March 20, 2017
author

Mirko Jotic

Testing Vuex-Dependent Vue.js Components

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 inject-loader@^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! 🎉🍕

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Mirko Jotic

author

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more