Testing with Vue.js

Marshall Shen
Babystep
Published in
6 min readAug 28, 2017

For the past year I have been on the team building progressive web application (PWA) using Vue.js. The framework is relatively new, so there is not a standard guideline on how to properly test a large scaled Vue application.

If you are building a large PWA with Vue.js as the frontend framework, I hope you find this blogpost useful. In this post I will share some of our learning lessons and pragmatical guidelines our team come up in tesing with Vue.js.

Challenge of scaling

It all starts with the growing pain. As we keep adding components to our front end app, we realize that we need to adopt flux architecture and introduce Vuex to scale. As we adopt Vuex and add more pages to the app, we realize that we need to introduce Vue Router to handle more sophisticated rendering. Very soon, our front end has odd behaviors, our QA start to get confused, and developers start to scream at each other — okay, maybe not scream, but we were definitely frustrated bunch.

Then comes the natural conversation of improve code quality, which leads to the conversation of add test coverage to our frontend. So the developers rolled up our sleeves and start to tackle of the problem of writing Javascript tests against a fairly large PWA in Vue. Experiments after another, code review after another, we came up with our solution, yet two thoughts stand true in our discovery, one good, one bad.

The bad: tooling of Javascript testing

First, let’s start with the bad. Regardless of which Javascript framework we use, when it comes to front end testing, there are many ala carte solutions out there, each may solve one aspect of testing very well, but none excel in all aspects of testing. Before we start building our own Javascript testing procedure, we come up with a checklist of what functionality we want when running the tests. Below is a list and what we settled on.

Package manager: Webpack, Yarn, and Rails asset pipeline

Test runner: Karma

Assertion: Chai.js

Mocks and stubs: Sinon.js

The testing solution we come up feels piece-meal, which reflects the fragmented nature of Javascript testing framework. Within Vue.js community people have been working on Vue specific testing framework such as Vue test utils, but as of the time of this writing, the project is fairly young.

A sample test inside the app looks like below, the describe and it blocks are provided by Karma.js. The expect clause is provided by Chai.js. Note that this piece-meal solution should also apply to other frameworks such as Angular and React.

describe("formatDates", () => 
{
const app = window.app
const expect = window.chai.expect
const subject = window.app.mixins.formatDates
const moment = window.moment
describe("#formatDate", () => {
it("returns a date formatted as MM.DD.YYYY", () => {
const date = moment().format()
const formattedDate = moment.utc(date).format("MM.DD.YYYY")
expect(subject.methods.formatDate(date))
.to.eq(formattedDate)
})
})

describe("#formatUtcDate", () => {
it("returns an utc formatted date", () => {
let startDate = moment().format()
let value = subject.methods.formatUtcDate(startDate)
let formattedDate = moment.utc(startDate).format()

expect(value).to.eql(formattedDate)
})
})
})

The Good: component tests and interaction tests

Okay, let’s go into the good thought. We realize that component-based framework allows us to test our Javascript code differently. Traditionally, we write unit test, integration test and end to end test around Javascript application. Although those scopes of test might still hold true while testing with Vue, we like to think that we can write two types of tests with Vue.js: component tests and interaction tests.

At its core, a Vue app is composed of components and their interactions, and the ideas below hold true for each component:

  1. each component owns its view
  2. each component gets its data locally (props) or globally (Vuex)
  3. each component handles events by emitting events, or updating local or global data

In a large scale web application, Vue.js follows flux architecture with core plugin Vuex to handle state management, and our tests should leverage the design of flux architecture.

Component tests

Component tests focus on functionality of a component, which is similar to unit tests. Specifically, we want to verify that:

  1. Data. A component has correct data, computed property, props loaded at a given life cycle. This may require correct setup of Vuex store, and also the calling of correct lifecylce method such as mount.
  2. Events. A component can properly handle different events by either emitting events or change data. This may require correct setup of Vuex store, and properly stubs and spys of event listeners.

Below is an example of verifying the correct of data.

describe("Account Setting Component", () => { 
const Vue = window.Vue
const Vuex = window.Vuex
const AccountSettingComponent = window.app.components.AccountSetting
const store = new Vuex.Store({
state: {
account: { settings: { display_settings: true } }
}
})
let vm beforeEach(() => {
const Ctor = Vue.extend(AccountSettingComponent)
vm = new Ctor({
template: "<div id='vue-account-settings-component'></div>",
propsData: {},
store: store
})
vm.$mount()
})
describe("#mounted()", () => {
it("fetches display settings from the store", () => {
expect(vm.displaySettings).to.equal(true)
})
})
})

At the beginning of the test, we explictly require dependencies around Vue, including the Vue, Vuex and the component that we try to render. In addition, we set up const store with a JSON object, which we only mock the minimum amount of data (i.e. state) that is required within the test.

Inside beforeEach() block we call the constructor of the component by using Vue.extend, and AccountSettingComponent is nothing but a JSON object, a more detailed doc is here. In the end, we can $mount() so that the we “activate” the component.

Let’s say we want to check the correctness of event handling, we have a method called handleDisplaySettingToggle that toggles display_setting on and off, we can fire off the event from the component and verify it from data.

describe("Account Setting Component", () => { 
const Vue = window.Vue
const Vuex = window.Vuex
const AccountSettingComponent = window.app.components.AccountSetting
const store = window.app.store
let vm beforeEach(() => {
const Ctor = Vue.extend(AccountSettingComponent)
vm = new Ctor({
template: "<div id='vue-account-settings-component'></div>",
propsData: {},
store: store
})
vm.$mount()
})
...
describe("#methods", () => {
describe("handleDisplaySettingsToggle", () => {
it("toggles display settings", () => {
vm.handleDisplaySettingsToggle(false)
expect(vm.displaySettings).to.eql(false)
})
})
})
})

Interaction tests

Interaction tests focus on interactions between components, router and store. This is close to integration test. Specifically, we can verify that:

  1. State change. An event inside one component dispatches actions to other components or store, resulting state change in other components.
  2. Server client side interaction. An action dispatched triggers an ajax with specific parameters. Upon receiving expected response from the server, which we can stub out, then the app renders correct data on the view.
describe("Interaction tests: account settings and account review", () => { 
const Vue = window.Vue
const Vuex = window.Vuex
const AccountSettingComponent = window.app.components.AccountSetting
const AccountReviewComponent = window.app.components.AccountReview
let vm
const store = new Vuex.Store({
state: {
account: { settings: { display_settings: true } }
}
})
beforeEach(() => {
const Ctor1 = Vue.extend(AccountSettingComponent)
vm1 = new Ctor1({
template: "<div id='vue-account-settings-component'></div>",
propsData: {},
store: store
})
vm1.$mount() const Ctor2 = Vue.extend(AccountReviewComponent)
vm2 = new Ctor2({
template: "<div id='vue-account-review-component'></div>",
propsData: {},
store: store
})

vm2.$mount() })
...
describe("display_seetings", () => {
it("toggles display settings", () => {
vm1.handleDisplaySettingsToggle(false)
expect(vm2.displaySettings).to.eql(false)
})
})
})

The key for the interaction test is that both vm1 and vm2 share the same store so that the state change go through the same store.

Note that neither component tests nor interaction tests verifies what is actually rendered on the DOM. This is the beauty of component-based framework: if a component is populated with correct data and handles events the right way, the view should take care of itself.

Marshall Shen © 2017

Originally published at himarsh.org on August 28, 2017.

--

--