Testing with Vue.js
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:
- each component owns its view
- each component gets its data locally (props) or globally (Vuex)
- 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:
- 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. - 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:
- State change. An event inside one component dispatches actions to other components or store, resulting state change in other components.
- 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.