Edd Yerburgh Software Engineer

stub $route in Vue unit tests

In this article we’ll look at four ways to stub $route in Vue unit tests. One naive approach, one enlightened, one using avoriaz and one using the unreleased vue-test-utils library.

Note: We won’t explore basic unit testing concepts. If you aren’t familiar with unit testing Vue components, check out How to unit test Vue components

The component

<template>
  <div></div>
</template>

<script>
export default {
  name: 'test-component'
}
</script>

We need to test that this.$route.name is rendered inside the div.

To do that we have to stub $route.

Stub $route - the naive approach

To stub $route we’ll pass a mock $route object to Vue that is accessible from our component instance.

We can do this by adding the mock object to the Vue prototype.

In Vue, all instances are created from the Vue base constructor. If we add a property to the Vue prototype each child Vue instance (vm) will have the $route property when it is created.

Vue.prototype.$route = { name: 'route-name' }

A unit test will look like this:

import Vue from 'vue'
import { expect } from 'chai'
import Component from './component.vue'

const $route = {
  name: 'test name - naive'
}
Vue.prototype.$route = $route

const Constructor = Vue.extend(Component)
const vm = new Constructor().$mount()
expect(vm.$el.textContent).to.equal($route.name)

This will pass. But every Vue component mounted in tests after this will have $route defined.

Let’s see how to add $route without affecting other tests.

Stub $route - the enlightened approach

In our previous test we used Vue.extend() to create a constructor for the component we are testing.

Vue.extend() returns a subclass of the instance it was called on. This means we can call it on the base Vue constructor to create a subclass of Vue, add $route to the subclass prototype and extend the subclass to create our component instance.

That explanation was a bit wordy, so let’s see it in action:

import Vue from 'vue'
import { expect } from 'chai'
import Component from './component.vue'

it('renders $router.name', () => {
  const scopedVue = Vue.extend()
  const $route = {
    name: 'test name - enlightened'
  }
  scopedVue.prototype.$route = $route

  const Constructor = scopedVue.extend(Component)
  const vm = new Constructor().$mount()
  expect(vm.$el.textContent).to.equal($route.name)
})

We create a scopedVue subclass, add $route to the prototype of the subclass and then create a component constructor from the subclass. $route will exist on the component instance, but not the base Vue constructor.

Great! Now we’re containing the effects of our test. No other Vue instances created with the base Vue constructor will be affected.

It’s kind of cryptic though. Let’s see what it looks like using libraries.

Stub $route with avoriaz

avoriaz is a test util library for Vue. It’s aim is to make testing Vue copmponents easier.

This is our test rewritten with avoriaz:

import Vue from 'vue'
import { expect } from 'chai'
import { shallow } from 'avoriaz'
import Component from './component.vue'

it('renders $router.name', () => {
  const instance = Vue.extend()
  const $route = {
    name: 'test name - avoriaz'
  }
  const wrapper = shallow(Component, {
    globals: { $route },
    instance
  })
  expect(wrapper.text()).to.equal($route.name)
})

The shallow method takes a component, stubs all of it’s child components and returns a wrapper around a mounted vue instance.

shallow takes options that affect the mounted component. Here we’re passing globals and instance. globals adds properties to the Vue class, and instance is the base instance avoriaz will use to mount on.

Our tests are easier to understand and more robust with avoriaz. What about an official solution?

Stub $route with vue-test-utils

vue-test-utils is the official Vue test library. As of writing (June 2017) it hasn’t been released, but it’s in development.

The library is based on avoriaz, so the API is very similar.

Let’s look at the same test using vue-test-utils:

import { expect } from 'chai'
import {
    scopedVue,
    shallow
} from 'vue-test-utils'
import Component from './component.vue'

it('renders $router.name', () => {
  const instance = scopedVue()
  const $route = {
    name: 'test name - avoriaz'
  }
  const wrapper = shallow(Component, {
    instance,
    intercept: { $route }
  })
  expect(wrapper.text()).to.equal($route.name)
})

There are two differences between this test and the one written with avoriaz. The scopedVue function, and intercept option.

intercept behaves the same way as globals. It’s just a more descriptive name.

scopeVue returns a Vue instance. Why are we using scopedVue instead of Vue.extend()? Vue.extend doesn’t duplicate every property from the base Vue constructor. It’s missing properties like config, versionNumber and utils.

You can see the function here.

Gotchas

When you install vue-router, it adds a $route to the root Vue class. It’s added as a read only property, meaning it can’t be overwritten.

If you install vue-router by using Vue.use(VueRouter) anywhere in your test suite, you will not be able to rewrite the $route object.

Conclusion

We saw 4 ways of stubbing this.$route in Vue tests.

A repo with the tests is available here

If you have any questions, leave a comment.