This post was published in 2017, and some information may now be outdated.
When building a web-app in Vue or any other JS Framework, it’s important to make sure Vue components work as intended throughout the many iterations a project can have. Vue.js allows a very easy and ready to go testing setup with Vue Webpack Template, which already includes a boilerplate for unit tests (using Karma and Mocha) and E2E tests (using Nightwatch). The purpose of this article is to provide different unit test examples on various domains using Karma and Mocha. But first, it’s important to understand some basic JS testing keywords.
Component Tests — Using Vue instance
A component has many structures that should be tested:
- Lifecycle hooks: For example, test if a function is called when the component is mounted, updated, …
- Methods: Test if the function’s return is the expected or if the changes on data were correctly made.
- Watchers: When changing a prop or data value, check if the watcher is invoked.
- Computed properties: Check if the computed property is returning the intended value.
Components can have multiple functions that use each other as a dependency and this can turn into some complex behaviors. So, it’s important to:
- Try to make small, easy to test functions (too many dependencies or complex functions make tests harder to write — and the code harder to read as well).
- A function should return a value or change a field on the component’s data in order to be easier to test.
- Control your test environment. Focus on testing a single unit of code, mocking the environment or dependencies invocations. Using shallow instead of mount helps to control how the component’s instance renders. When a component is connected to store (either by mapActions, mapState or mapGetters) it’s really important to mock the store and pass it as a global parameter to the component’s instance.
When a component instance is created, a wrapper around the component instance (also named vm) is created and it provides a great API to edit props, data, and many other properties.
Be aware that avoriaz syntax (which is very similar to vue-test-utils syntax) is used in the following tests.
Testing Lifecycle Hooks
A Vue component has multiple lifecycle hooks and to test those, actions that trigger them must be done (create the instance to test beforeMount and mounted while destroying it will trigger the beforeDestroy and destroyed).
Using this component:
-- CODE language-html line-numbers--
< template > <br>
< div > < /div > <br>
< template ><br>
< script ><br>
export default {<br>
// == Lifecycle<br>
mounted () {<br>
window.addEventListener('scroll', this.handleScroll)<br>
},<br>
destroyed () {<br>
window.removeEventListener('scroll', this.handleScroll)<br>
},<br>
// == Methods<br>
methods: {<br>
handleScroll () {<br>
console.log('handleScroll')<br>
return<br>
}<br>
}<br>
}<br>
< /script ><br>
To check if mounted and destroyed hooks work as intended, an instance of this component should be created and destroyed. Since stub can be applied not only to the component’s instance (and therefore the component’s methods) but also to the window, it’s possible to verify if events are being handled as well.
On the test below it’s possible to see how the lifecycle hooks are tested:
-- CODE language-js line-numbers --
describe('Basic component', () => { <br>
describe('Lifecycle', () => {<br>
it('Mounted', done => {<br>
const handleScrollStub = sinon.stub(BasicComponent.methods, 'handleScroll')<br>
const addEventStub = sinon.stub(window, 'addEventListener')<br>
const wrapper = mount(BasicComponent)<br>
expect(addEventStub).to.be.calledWith('scroll', wrapper.vm.handleScroll)<br>
handleScrollStub.restore()<br>
addEventStub.restore()<br>
done()<br>
})<br>
it('Destroyed', done => {<br>
const wrapper = mount(BasicComponent)<br>
const handleScrollStub = sinon.stub(wrapper.vm, 'handleScroll')<br>
const removeEventStub = sinon.stub(window, 'removeEventListener')<br>
wrapper.destroy()<br>
expect(removeEventStub).to.be.calledWith('scroll', wrapper.vm.handleScroll)<br>
handleScrollStub.restore()<br>
removeEventStub.restore()<br>
done()<br>
})<br>
})<br>
})<br>
Creating spies or stubs on the component (BasicComponent — as you see in the Mounted example) instead of creating them on the instance (wrapper.vm) is useful when you want to mock / spy on a dependency that will be invoked on the instance mount lifecycles (like beforeMount or mounted).
Testing Methods
Methods should always have a clear way of being tested, either by returning a value / Promise, changing a component’s data or controlling other functions invocations. Thinking of how a function will be tested will very probably improve the code’s quality and maintainability.
Many times functions behavior changes depending on a parameter value or other function’s invocation. The context should be mocked (probably using stubs) to test all use cases.
-- CODE language-js line-numbers --
< template > <br>
< div > < /div > <br>
< /template > <br>
< script type="text/javascript" > <br>
export default {<br>
...<br>
data () {<br>
return {<br>
currentObjects: [],<br>
hasError: false<br>
}<br>
},<br>
methods: {<br>
getObjects () {<br>
...<br>
},<br>
setObjects () {<br>
const objectsOnStore = this.getObjects()<br>
if(objectsOnStore) {<br>
this.currentObjects = objectsOnStore<br>
return true<br>
} else {<br>
this.hasError = true<br>
return false<br>
}<br>
}<br>
}<br>
}<br>
< /script > <br>
In the above scenario, setObjectsfunction will have a behavior depending on getObjects result. In order to test both use cases, the return value of getObjects invocation should be controlled as seen in this test:
-- CODE language-js line-numbers --
describe('Objects component', () => { <br>
describe('Methods', () => {<br>
it('getObjects - should return true and set the data.currentObjects if getObjects retrieves an array of objects', done => {<br>
const mockedObjects = [<br>
{ id: 1 },<br>
{ id: 2 }<br>
]<br>const wrapper = mount(ObjectsComponent) <br>
const getObjectsStub = sinon.stub(wrapper.vm, 'getObjects').returns(mockedObjects)<br>
const result = wrapper.vm.getObjects()<br>
expect(result).to.be.true<br>
expect(wrapper.data().currentObjects).to.be.deep.equal(mockedObjects)<br>
getObjectsStub.restore()<br>
done()<br>
})<br>
it('getObjects - should return false and set the data.hasError to true if getObjects retrieves an empty array', done => {<br>
const mockedObjects = []<br>
const wrapper = mount(ObjectsComponent) <br>
const getObjectsStub = sinon.stub(wrapper.vm, 'getObjects').returns(mockedObjects)<br>
const result = wrapper.vm.getObjects()<br>
expect(result).to.be.false<br>
expect(wrapper.data().hasError).to.be.true<br>
getObjectsStub.restore()<br>
done()<br>
})<br>
})<br>
})<br>
When using a function that depends on an API call, an approach to take is to use a Promise. In this scenarios, the best way to proceed is to return the Promise.
The following component is connected to store. The store should be completely mocked in order to control the test’s context and avoid any kind of errors.
-- CODE language-js line-numbers -- < template > <br>
< div > < /div > <br>
< /template > <br>
< script type="text/javascript" > <br>
import { GET_OBJECTS } from 'services/constants/action-types'<br>
export default {<br>
...<br>
data () {<br>
return {<br>
currentObjects: []<br>
}<br>
},<br>
methods: {<br>
...mapActions('objects', {<br>
getObjectsAction: GET_OBJECTS<br>
}),<br>
setObjects () {<br>
return this.getObjectsAction()<br>
.then((response) => {<br>
this.currentObjects = response<br>
})<br>
}<br>
}<br>
}<br>
< /script > <br>
Returning the Promise allows the API call to be stubbed and this way, it triggers the Promise’s resolve. All the assertions should run on the callback.
Considering that getObjectsAction returns a Promise (that will perform the API request on the action), a good way to test this function would be:
--CODE language-js line-numbers --
import { GET_OBJECTS } from 'services/constants/action-types'<br>
describe('Objects component', () => {<br>
let store<br>
let state<br>
let actions<br>
beforeEach(() => {<br>
actions = {}<br>
actions[GET_OBJECTS] = sinon.stub()<br>
state = {<br>
modules: {<br>
// mock state, with actions and getters if any<br>
objects: {<br>
namespaced: true,<br>
state: {<br>
objs: []<br>
},<br>
actions<br>
}<br>
}<br>
}<br>
store = new Vuex.Store(state)<br>
})<br>
describe('Methods', () => {<br>
it('getObjects - should set data.currentObjects with response returned from getObjects', done => {<br>
const mockedObjects = [<br>
{ id: 1 },<br>
{ id: 2 }<br>
]<br>
const wrapper = shallow(ObjectsComponent, {<br>
store<br>
})<br>
const getObjectsStub = sinon.stub(wrapper.vm, 'getObjects').returns(mockedObjects)<br>
wrapper.vm.getObjects()<br>
.then((result) => {<br>
expect(result).to.be.true<br>
expect(wrapper.data().currentObjects).to.be.deep.equal(mockedObjects)<br>
getObjectsStub.restore()<br>
done()<br>
})<br>
})<br>
})<br>
})
This approach (returning a Promise or a chain of Promises) is very useful in scenarios where requests need to be mocked to avoid timeout errors and allowing to test how a function will perform after the Promise’s resolve. Instead of resolving the Promise, reject it will also trigger the catch scenario on the test.
Testing Watchers
A watcher is used to react to data changes and it can be applied to a component’s props or data. To trigger watchers on the test, setData or setProps methods can be used.
-- CODE language-js line-numbers --
< template > <br>
< div > < /div > <br>
< /template > <br>
< script type="text/javascript" > <br>
export default { <br>
props: {<br>
propId: {<br>
type: Number,<br>
required: true<br>
}<br>
},<br>
methods: {<br>
consoleLogValue(value) {<br>
console.log(value)<br>
}<br>
},<br>
watch: {<br>
propId (newVal) {<br>
this.consoleLogValue(newVal)<br>
}<br>
},<br>
}<br>
< /script > <br>
Changing propId will trigger the watcher.
This test will only check if consoleLogValue is being invoked with the correct parameter using calledWith as there is no intention to verify how consoleLogValue behaves. That should be done in another test. For this reason, a stub is applied to consoleLogValue.
-- CODE language-js line-numbers --
describe('Watcher component', () => { <br>
let props<br>
beforeEach(() => {<br>
props = {<br>
propId: 1<br>
}<br>
})<br>
describe('Watcher', () => {<br>
it('propId', done => { <br>
const wrapper = shallow(WatcherComponent, {<br>
propsData: props<br>
})<br>
const consoleLogValueStub = sinon.stub(wrapper.vm, 'consoleLogValue')<br>
wrapper.setProps({<br>
propId: 2<br>
})<br>
expect(consoleLogValueStub).to.be.calledWith(2)<br>
consoleLogValueStub.restore()<br>
done() <br>
})<br>
})<br>
})<br>
Testing computed properties
It’s usual to use computed properties to retrieve a value depending on one or multiple props. Computed properties usually represent simple operations that shouldn’t be placed on the template to be easier to maintain.
-- CODE language-js line-numbers --
< template > <br>
< div :class="themeDiv" > < /div > <br>
< /template > <br>
< script type="text/javascript" > <br>
export default { <br>
props: {<br>
plan: {<br>
required: true,<br>
type: String<br>
}<br>
},<br>
computed: {<br>
themeDiv: function () {<br>
if (this.plan === 'pro') {<br>
return 'blue'<br>
} else if (this.plan === 'trial') {<br>
return 'soft-blue'<br>
} else {<br>
return 'orange'<br>
}<br>
}<br>
}<br>
}<br>
< /script > <br>
To access computed properties, we can use the component instance directly (wrapper.vm.computedProperty). That said, setProps can be used to test all the branches:
<p>-- CODE language-js line-numbers --
describe('Computed component', () => { <br>
let props<br>
beforeEach(() => {<br>
props = {<br>
plan: 'pro'<br>
}<br>
})<br>
describe('Computed', () => {<br>
it('themeDiv', done => { <br>
const wrapper = shallow(ComputedComponent, {<br>
propsData: props<br>
})<br>
expect(wrapper.vm.themeDiv).to.equal('blue')<br>
wrapper.setProps({<br>
theme: 'trial'<br>
})<br>
expect(wrapper.vm.themeDiv).to.equal('soft-blue')<br>
wrapper.setProps({<br>
theme: 'standard'<br>
})<br>
expect(wrapper.vm.themeDiv).to.equal('orange')<br>
done()<br>
})<br>
})<br>
}) </p>
Component Tests — Using the Object
As explained before, Vue components’ attributes are all functions. This mindset will make it easier to approach the tests we only need to know how is the object composed. Other than that we’re just testing a function, a small unit of code that shouldn’t have many dependencies.
Considering the following component:
<p>-- CODE language-js line-numbers --
< template > <br>
< div :class="getClasses" > <br>
Another component, another test, random probability<br>
< /div > <br>
< /template > <br>
< script type="text/javascript" > <br>
export default {<br>
props: {<br>
classModifiers: {<br>
type: Array,<br>
default: () => ['large']<br>
}<br>
},<br>
computed: {<br>
baseProbability () {<br>
if (this.classModifiers.includes('large')) {<br>
return .8<br>
}<br>
return .5<br>
},<br>
getClasses () {<br>
return this.classModifiers.map(function (class) {<br>
return `probability--${modifier}`<br>
})<br>
}<br>
},<br>
methods: {<br>
calcProbability (probMultiplier) {<br>
return this.baseProbability * probMultiplier<br>
}<br>
}<br>
}<br>
< /script > <br>
</p>
In order to test a function that requires values from props or from component’s data (as the Vue instance will not be created now), a variable must be created with this values and passed to the tested function. On the following tests, this variable will be named ‘context’.
<p>-- CODE language-js line-numbers --
// Working with the object here! <br>
import Probability from '../probability.vue'<br>
describe('Probability component', () => {<br>
describe('Computed ', () => {<br>
it('getClasses', done => {<br>
const context = {<br>
classModifiers: ['medium', 'blue-theme', 'rounded-corners']<br>
}<br><br>
expect(Probability.computed.getClasses.call(context))<br>
.to.be.eql(['probability--medium', 'probability--blue-theme', 'probability--rounded-corners'])<br>
done()<br>
})<br><br>
it('baseProbability - should return 0.8 when "large" is on the classModifiers list', done => {<br>
const context = {<br>
classModifiers: ['large', 'blue-theme', 'rounded-corners']<br>
}<br><br>
expect(Probability.computed.baseProbability.call(context)).to.be.eq(.8)<br>
done()<br>
})<br><br>
it('baseProbability - should return 0.5 when "large" is not on the classModifiers list', done => {<br>
const context = {<br>
classModifiers: ['medium', 'blue-theme', 'rounded-corners']<br>
}<br><br>
expect(Probability.computed.baseProbability.call(context)).to.be.eq(.5)<br>
done()<br>
})<br>
})<br><br>
describe('Methods ', () => {<br>
it('calcProbability', done => {<br>
const context = {<br>
baseProbability: .5<br>
}<br><br>
expect(Loading.methods.calcProbability.call(context, 100)).to.be.eq(50)<br>
done()<br>
})<br>
})<br>
})
</p>
Without a Vue instance, invoking functions uses the object’s properties (Object.computed, Object.methods, etc). The ‘context’ variable allows the tested function to still use the expected values from the Vue instance data structure.
The lifecycles hooks can also be invoked using the object’s properties. Operations like using functions created on mapActions can be placed on the ‘context’ variable to avoid mocking the store (one of this approach’s major advantages). Using the following component:
<p>-- CODE language-js line-numbers --
< template > <br>
< div > <br>
Lifecycle using object example<br>
< /div > <br>
< /template > <br>
< script type="text/javascript" > <br>
import { mapActions } from 'vuex'<br>
import { actionTypes } from 'services/constants'<br><br>
export default {<br>
methods: {<br>
...mapActions('app', {<br>
getInitialConfig: actionTypes.APP_GET_CONFIG<br>
})<br>
},<br>
created () {<br>
this.getInitialConfig()<br>
}<br>
}<br>
< /script > <br>
</p>
The getInitialConfig function will be placed inside the ‘context’ variable and therefore, a spy is created to understand if the function is called when the created hook is invoked.
<p>-- CODE language-js line-numbers --
import Obj from '../Obj.vue'<br>
describe('Obj', () => {<br>
describe('Lifecycle', () => {<br>
it('created', done => {<br>
const context = {<br>
getInitialConfig: () => {}<br>
}<br>
const getInitialConfigSpy = sinon.spy(context, 'getInitialConfigSpy')<br>
Obj.created.call(context)<br>
expect(getInitialConfigSpy).to.be.calledOnce<br>
getInitialConfigSpy.restore()<br>
done()<br>
})<br>
})<br>
})<br>
</p>
Conclusion
Unit testing can sometimes be tricky and if you’re starting, it will surely cause some pain. But in the end, it’s worth it.
Tests bring great advantages to the project and to the developer’s evolution. The developer’s mindset will change to write better, easier to test and more maintainable code using pure functions, following the Do One Thing (DOT) rule instead of overloading a unit of code with multiple responsibilities and taking better decisions over the structure and scalability of an application.
This article focused on showing some examples of how to approach various Vue.js testing domains, more like an easy to remind / understand how to approach a certain scenario and to help you all understand what it’s being done when writing a test. Hope it was helpful!