Vue.js Test from dust to light

Di.rk
7 min readApr 11, 2018

Yeah

First of all I love Vue and everything that it surrounds. It feels so natural and easy to use compared to Angular, I worked with for some years.

It’s the best SPA framework I get to know so far and thank you to all the developers who makes everything possible, not only Vue.js.

Time to give some back and I hope it helps a little bit those who want’s to get started with unit testing in Vue.js.

Start

Okay let’s start.

I have some experience in unit testing in Angular and so I was not very sure if this gets also as complicated. But don’t be afraid as this it is very straighforward and believe me you are able to test your code as me and it is worth it.

So I will not begin with an easy case that has to be tested, rather I will jump into a not so simple component that uses Vuetify, Lodash and translation.

What to test? LocaleMenu.

It is a small component that displays the selected locale and also shows a locale menu and handles the change of the locale. I have put it into a directory with the name LocaleMenu and provide it with an index.js. It looks like this:

The index.js simply import and exports the component and I use this because it makes my imports simpler:

So my index.js looks like this:

import LocaleMenu from './LocaleMenu'
export default LocaleMenu

The component itself is implemented in LocaleMenu.vue:

<template>
<v-menu offset-y>
<v-btn icon slot="activator">
<img :src="currentLanguageItem.imageUrl">
</v-btn>
<v-list>
<v-list-tile v-for="(language, id) in languageItems" :key="id" @click.stop.prevent="setLanguage(id)">
<v-list-tile-action>
<img :src="language.imageUrl">
</v-list-tile-action>
<v-list-tile-title>{{language.name()}}</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
</template>

<script>
import supportedLocales from '@/shared/i18n/supportedLocales'

export default {

computed: {
currentLanguageItem () {
return supportedLocales[this.$i18n.locale()]
},
languageItems () {
// without current selected language
return this._.omitBy(supportedLocales, (languageItem, key) => key === this.$i18n.locale())
}
},

methods: {
setLanguage (language) {
this.$i18n.set(language)
}
}
}
</script>

<style scoped>
</style>

And because there I use another file, I should show this also:

import Vue from 'vue'
import translationsDe from '@/assets/i18n/de.json'
import translationsEn from '@/assets/i18n/en.json'

// to support another locale simply add a translation file and an entry
const supportedLocales = {
'de-DE': {
name: () => Vue.i18n.translate('languages.german'),
translations: translationsDe,
imageUrl: 'https://countryflags.io/de/flat/24.png'
},
'en-US': {
name: () => Vue.i18n.translate('languages.english'),
translations: translationsEn,
imageUrl: 'https://countryflags.io/us/flat/24.png'
}
}

export default supportedLocales

That should be enough to understand my tests written for the component.

So here we go — a test.

I’ve started with a basic test environment:

import {expect} from 'chai'
import {shallow} from '@vue/test-utils'
import LocaleMenu from '@/shared/components/LocaleMenu'

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu)
expect(true).to.be.false
})
})

The execution with npm run test leads me to the first problem:

Inspecting it in LocaleMenu.vue gives me a hint:

It is missing the translation component i18n, but where to get it.

I have to create a local Vue instance to add a mock — changes in bold.

import {expect} from 'chai'
import {createLocalVue, shallow} from '@vue/test-utils'
import LocaleMenu from '@/shared/components/LocaleMenu'

const localVue = createLocalVue()

const i18nMock = {
currentLocale: 'de-DE',

locale () {
console.log('locale', this.currentLocale)
return this.currentLocale
}
}


localVue.use(i18nMock)

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})

console.log(wrapper.html())
expect(true).to.be.false
})
})

After doing this running the test gives me

Wait I saw this method call earlier. I use this in the languageItems method of my component. That means it is also missing Lodash. Okay, got it.

import {expect} from 'chai'
import VueLodash from 'vue-lodash'
import {createLocalVue, shallow} from '@vue/test-utils'
import LocaleMenu from '@/shared/components/LocaleMenu'

const localVue = createLocalVue()

const i18nMock = {
currentLocale: 'de-DE',

locale () {
console.log('locale', this.currentLocale)
return this.currentLocale
}
}

localVue.use(i18nMock)
localVue.use(VueLodash)

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})
console.log(wrapper.html())
expect(true).to.be.false
})
})

Next round. Executing the test again creates a missing translate property:

At line 13 he wants to use the global instance:

Okay, that means I have to import Vue and add this to Vue.

import Vue from 'vue'
import {expect} from 'chai'
import VueLodash from 'vue-lodash'
import {createLocalVue, shallow} from '@vue/test-utils'
import LocaleMenu from '@/shared/components/LocaleMenu'

Vue.i18n = {
translate (id) {
console.log('translate', id)
return id
}
}


const localVue = createLocalVue()

const i18nMock = {
currentLocale: 'de-DE',

locale () {
console.log('locale', this.currentLocale)
return this.currentLocale
}
}

localVue.use(i18nMock)
localVue.use(VueLodash)

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})
console.log(wrapper.html())
expect(true).to.be.false
})
})

The next run gives me some warnings — did I mention that I use Vuetify?

Okay, get it also.

import Vue from 'vue'
import VueLodash from 'vue-lodash'
import Vuetify from 'vuetify'
import LocaleMenu from '@/shared/components/LocaleMenu'
import {expect} from 'chai'
import {createLocalVue, shallow} from '@vue/test-utils'

Vue.i18n = {
translate (id) {
console.log('translate', id)
return id
}
}
Vue.use(Vuetify)const localVue = createLocalVue()

const i18nMock = {
currentLocale: 'de-DE',

locale () {
console.log('locale', this.currentLocale)
return this.currentLocale
}
}

localVue.use(i18nMock)
localVue.use(VueLodash)

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})
console.log(wrapper.html())
expect(true).to.be.false
})
})

I have to add it to Vue because it it supported globally, not local — I am not importing the components in my component.

Here we go, I get an AssertionError — that we expect.

So now let’s make this test more useful, let’s test something.

import Vue from 'vue'
import Vuetify from 'vuetify'
import VueLodash from 'vue-lodash'
import LocaleMenu from '@/shared/components/LocaleMenu'
import {expect} from 'chai'
import {createLocalVue, shallow} from '@vue/test-utils'

Vue.i18n = {
translate (id) {
console.log('translate', id)
return id
}
}

Vue.use(Vuetify)

const localVue = createLocalVue()

const i18nMock = {
currentLocale: 'de-DE',

locale () {
console.log('locale', this.currentLocale)
return this.currentLocale
}
}

localVue.use(i18nMock)
localVue.use(VueLodash)

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})
// console.log('html', wrapper.html())
const menuEntry = wrapper.find('a img')
expect(menuEntry.attributes().src).to.include('/us/')

})
})

No it passes, but what is this complain.

Ah, we have no app, so let us create one.

import Vue from 'vue'
import Vuetify from 'vuetify'
import VueLodash from 'vue-lodash'
import LocaleMenu from '@/shared/components/LocaleMenu'
import {expect} from 'chai'
import {createLocalVue, shallow} from '@vue/test-utils'

const app = document.createElement('div')
app.setAttribute('data-app', true)
document.body.append(app)


Vue.i18n = {
translate (id) {
// console.log('translate', id)
return id
}
}

Vue.use(Vuetify)

const localVue = createLocalVue()

const i18nMock = {
currentLocale: 'de-DE',

locale () {
// console.log('locale', this.currentLocale)
return this.currentLocale
}
}

localVue.use(i18nMock)
localVue.use(VueLodash)

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})
// console.log('html', wrapper.html())
const menuEntry = wrapper.find('a img')
expect(menuEntry.attributes().src).to.include('/us/')
})
})

Execute again.

So that’s it, we got it green.

Add on, another test to check the locale change by clicking.

import Vue from 'vue'
import Vuetify from 'vuetify'
import VueLodash from 'vue-lodash'
import LocaleMenu from '@/shared/components/LocaleMenu'
import {expect} from 'chai'
import {createLocalVue, shallow} from '@vue/test-utils'

const app = document.createElement('div')
app.setAttribute('data-app', true)
document.body.append(app)

Vue.i18n = {
translate (id) {
// console.log('translate', id)
return id
}
}

Vue.use(Vuetify)

const localVue = createLocalVue()

const i18nMock = {
currentLocale: 'de-DE',

locale () {
// console.log('locale', this.currentLocale)
return this.currentLocale
},
set (locale) {
// console.log('set', locale)
this.currentLocale = locale
}

}

localVue.use(i18nMock)
localVue.use(VueLodash)

describe('LocaleMenu', () => {
it('shows all available languages, current local excluded', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})
// console.log('html', wrapper.html())
const menuEntry = wrapper.find('a img')
expect(menuEntry.attributes().src).to.include('/us/')
})

it('selects the english locale if we select it', () => {
const wrapper = shallow(LocaleMenu, {
localVue,
mocks: {
$i18n: i18nMock
}
})
// console.log(wrapper.html())
const menuEntry = wrapper.find('a')
menuEntry.trigger('click')
// console.log(wrapper.html())
expect(i18nMock.currentLocale).equals('en-US')
const currentLocaleButton = wrapper.find('button img')
expect(currentLocaleButton.attributes().src).to.include('/us/')
})

})

Thank you for keeping up so far, I hope it helps you to test in Vue.js.

P.S.: Created with vue-cli version 3, still in beta I think.

ORCID

--

--