Unit Test Vue Apps with Vue Test Utils — Vue Router and Vuex Mocks

John Au-Yeung - Jan 23 '21 - - Dev Community

Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62

Subscribe to my email list now at http://jauyeung.net/subscribe/

With the Vue Test Utils library, we can write and run unit tests for Vue apps easily.

In this article, we’ll look at how to write unit tests with the Vue Test Utils library.

Mocking $route and $router

We can mock the $route and $router objects to inject the Vue Router’s reactive properties into our mounted component.

For instance, we can write:

import { shallowMount } from '@vue/test-utils'

const Component = {
  template: `
    <div>
      <p>{{$route.path}}</p>
    </div>
  `
}

describe('Component', () => {
  it('renders the Component component with the path', async () => {
    const $route = {
      path: '/some/path'
    }
    const wrapper = shallowMount(Component, {
      mocks: {
        $route
      }
    })
    expect(wrapper.text()).toContain('/some/path')
  })

})
Enter fullscreen mode Exit fullscreen mode

We have the Component component that renders the $route.path value.

We just put that in the mocks property so that it’ll be set with the given value when we mount the component.

Testing Vuex in Components

We can test components that depends on Vuex.

For example, if we have a Vue app that has the following code:

main.js

import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

App.vue

<template>
  <div id="app">
    <button @click="$store.dispatch('increment')">add</button>
    <p>{{count}}</p>
  </div>
</template>

<script>
export default {
  name: "App",
  computed: {
    count() {
      return this.$store.state.count;
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Then we can test the code by writing:

example.spec.js

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import App from '@/App'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('App.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      increment: jest.fn(),
    }
    store = new Vuex.Store({
      actions
    })
  })

  it('dispatches "increment" action when button is clicked', async () => {
    const wrapper = shallowMount(App, { store, localVue })
    const button = wrapper.find('button')
    await button.trigger('click')
    expect(actions.increment).toHaveBeenCalled()
  })
})
Enter fullscreen mode Exit fullscreen mode

Once again, we call createLocalVue to create a local Vue instance we use for testing.

Then we have a beforeEach callback to set up the store and inject it into our app.

We inject the mocked store and the localVue object.

Then we get the button and trigger the click event on it.

And finally, we check that the increment action has been triggered after clicking the button.

Mocking Getters

Similarly, we can mock getters in our tests.

Given that we have the following code:

main.js

import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    increment(context) {
      context.commit('increment')
    }
  },
  getters: {
    count(state) {
      return state.count;
    }
  }
})

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

App.vue

<template>
  <div id="app">
    <button @click="$store.dispatch('increment')">add</button>
    <p>{{count}}</p>
  </div>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  name: "App",
  computed: {
    ...mapGetters(['count'])
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Then to test our code, we write:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import App from '@/App'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('App.vue', () => {
  let actions
  let store
  let getters

  beforeEach(() => {
    getters = {
      count: () => 2,
    }
    actions = {
      increment: jest.fn(),
    }
    store = new Vuex.Store({
      actions,
      getters
    })
  })

  it('dispatches "increment" action when button is clicked', () => {
    const wrapper = shallowMount(App, { store, localVue })
    expect(+wrapper.find('p').text()).toBe(getters.count())
  })
})
Enter fullscreen mode Exit fullscreen mode

We add the mock getter into out mock store object.

Then we get the p element’s text and sees if it matches what’s returned in the mocked getter.

Conclusion

We can mock Vuex and Vue Router in our tests so that our components can be tested in isolation.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .