OneSheep

Testing Vue Components with Jest

Have you been writing VueJS without proper tests in place? That is a dangerous path especially if you have any reasonable amount of business logic to handle. VueJS has been our tool of choice and combined with Laravel, it has delivered great results but all too often, it has kept us with our fingers crossed everything is tight as it seems on the UI. So it was with much joy that we bumped into Vue Test Utils quite recently, and got stuck in wiring tests. We learned a ton and would like to show how you can test VueJS using Jest framework by Facebook. Of course you can use other testing frameworks (Mocha, Ava, Tape, etc) as well, but we want to keep this post as straightforward as possible. In other words this is a crash course. And it will be in two parts as there is a bit to cover.

Setting up for testing

Follow the guide here: https://​vue​-test​-utils​.vuejs​.org/​e​n​/​g​u​i​d​e​s​/​t​e​s​t​i​n​g​-​S​F​C​s​-​w​i​t​h​-​j​e​s​t​.html which will get webpack properly handling vue files as well as installing the node modules we are going to use.

Unit Testing

We are going to create a very basic component that grows as we scale up our test so follow closely:

UserProfile.Vue

<template>
 <div>
  Current name is {{ name }} 

 </div>
</template>
<script>
  export default {
   data(){
    name: 'Chiko'
   }
 }
</script>

First off we want to be able to display the value of the name” binding in our html. The point of a unit test here is to see if we have the binding in VueJS working properly. We are not concerned with what the html looks like, as this is not an integration test, however Vue Test Utils does have that functionality baked in which we will explore in the second part.

Here is what our test looks like to test for the binding above:

import { shallow } from 'vue-test-utils'
import UserInfo from '@/components/UserInfo'

describe('User Profile Tests', () => {
let wrapper = shallow(UserInfo)

test('it has name attribute', () => {
  const name = wrapper.vm.name
  expect(name).toBe('Chiko')
 }
}

Couple of things here:

  1. Rendering a component shallow means we are not concerned with that component’s children, which results in a more focused unit test for each component, besides the obvious speed gain.
  2. Wrapper.vm gives us direct access to the Vue instance which has all the data and all the other goodies from the component.

Let’s expand our component and include some props that it should expect:

We can easily set our propsdata at declaration and test for that too:

//Component
<script>
 export default {
  props: ['friendsList],
  ...
 }
</script>

//expand test
test('it receives friendsList prop', () => {
 const friendsList = {
  "john": [],
  "mary": []
 }

 wrapper = shallow(CreateSermon, {
  propsData: {
   'friendsList': friendsList
  }
 })
 expect(wrapper.vm.friendsList).toHaveProperty("john")
 expect(wrapper.vm.friendsList).toHaveProperty("mary")
})

Testing lifecycle hooks

So far we have been dealing with data that we assume is on the component, and that has nothing to do with Vue’s lifecycle methods. It’s often normal to have an API fetch something that we set on the component so how do we handle that?

Let’s expand and fetch the user’s likes via an axios call, add the following to our snippet:

export default {
 props: ['friendsList'],
 data(){
  name: 'Chiko',
  likes: []
 },
 created(){
  axios.get('/users/id/likes').then((response) => {

   //save a user's likes
   this.likes = response.data

  })
 }
}

We have a couple of ways to handle this scenario, the best one I think is to spy on our component and make sure that the method is called in the first place as follows:

const createdHook = jest.SpyOn(UserProfile, 'created')
//we set up a spy before we even mount the component, so that when it's mounted, we can see the implementation.

wrapper = shallow(CreateSermon)
expect(createdHook).toHaveBeenCalled()

That is not the last of our problems however because of the axios.get method which the created hook actually calls at runtime. We will get an error in the terminal with something along the lines of: ReferenceError: axios is not defined

Setting up mocks with Jest

My solution is to mock or stub any functionality with potential side effects. Depending on what your wepback configuration is, I have Jest setup to run a file before all tests to set up the testing environment. This allows one to have stuff like axios, localStorage / global set up and nicely mocked. Using the Configuring Jest setup instructions I have a mocked implementation of axios as follows:

// INSIDE YOUR SETUP.JS

const axiosMock = () => {
 return {
  get: () => new Promise((resolve, reject) => {}),
  post: () => new Promise((resolve, reject) => {})
 }
}

Object.defineProperty(window, 'axios', {value: axiosMock()});

Now if you run the test, it should succeed because when the created lifecycle hook is run, it simply calls global.axios which doesn’t return anything from that call. Is that a problem, no, because we can mock that call’s implementation too to see the route called and any other parameters each axios call should get as follows:

const userLikes = jest.spyOn(global.axios, 'get')
userLikes.mockImplementationOnce((route) => {
 expect(route).toBe('/users/id/likes')
 return new Promise((resolve, reject) => {})
})

const stubbedCreate = jest.spyOn(UserProfile, 'created)
wrapper = shallow(UserProfile)
expect(stubbedCreate).toBeCalled()

Testing vue’s methods

We also can have methods on the vue’s instance that require special handling. With unit testing after all, the idea is to test how our functions behave and what data they output. Let’s expand the example to include a method that depends on a computed property and tells the user a custom message:

export default {
 props: ['friendsList'],
 data(){
  name: 'Chiko',
  likes: [],
  greeting: ''
 },
 computed: {
  timeOfday(){
   // here we have logic that returns "afternoon" or "evening for demonstration purposes
   const customizedTime = (time) => friendlyTime 
   return customizedTime
  }
 },
 created(){
  axios.get('/users/id/likes').then((response) => {

  //save user likes
  this.likes = response.data

  })
 },
 methods: {
  setGreeting(){
   if(this.timeOfDay == "afternoon")
    return `hey ${this.name}, time to go for a swim!`
   return `hey ${this.name}, close the windows and curtains!`
 },
}

Testing this kind of functionality requires us to have predictable code, however a computed property will always change due to a certain condition, in this case: time. The best way to test what setGreeting() produces is to supply different timeOfDay values to it. Therefore we have to stub it instead as follows:

wrapper = shallow(UserProfile);
let afternoon = sinon.stub(UserProfile.computed, 'timeOfDay').returns("afternoon")
let afternoonMsg = wrapper.vm.setGreeting()
expect(afternoonMsg).toBe('hey Chiko, time to go for a swim!')
let evening = sinon.stub(UserProfile.computed, 'timeOfDay').returns("evening")
eveningMsg = wrapper.vm.setGreeting()
expect(eveningMsg).toBe('hey Chiko, close the windows and curtains!')

Posted on Apr 16, 2018 by Chiko Mukwenha

Back to all posts