Unit Testing with Vue.js

Basic Notions

  • Expect: Used to construct assertions, compare a value with the expected result on a test. Chai Assertions.

  • Spy: A test spy is an object that records its interaction with other objects and can be used to check if a certain function was called, the arguments passed to it (if any) and what the return value is (once again, if any). Sinon Spies.

  • Stub: Change how the function is called on the tests. It replaces a function’s behavior, avoiding the original function invocation. Can be used to test how our unit behaves to different return values from a dependency function. Sinon Stubs.

  • Mount: When mounting a component, an instance is created. The component is rendered as well as its child components.

  • Shallow: Very similar to mount but child components are stubbed, not rendered or instanced. Very useful in order to reduce the dependencies of a component’s test.

Testing Methodologies

Vue components’ attributes are all functions.

We can approach Vue unit testing in two major ways:

  • Instance oriented: The component’s instance is created. Big need to control the context as there are some behaviors set by Vue (for example, lifecycle hooks being run).

  • Object oriented: The component’s instance is not created. The context is easier to control as every test unit (a function for example) is tested as an isolated piece of code and doesn’t require such a higher amount of mocking and stubs.

Store Tests

The store has three main structures that should be tested. Each can focus on a question that the test should answer to:

  • Actions: What mutations are called from the action and what’s the payload? (the testAction function will be used for this purpose: https://vuex.vuejs.org/en/testing.html)

  • Mutations: Was the state changed the way it was expected to?

  • Getters: Is the getter retrieving the data correctly?

Some tips can also be found on vue-test-utils documentation.

Testing Actions

In simple scenarios, action tests can only check the mutation payload without worrying about the current state or other dependencies like an API call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// the action
[actionTypes.ACTION_SET_FAV_ELEM] (context, elemId) {
context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, elemId)
}

// the test
it('should invoke mutation to set the favourite element', done => {
const elemId = 'AAAAAA'
testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], elemId, {}, [
{ type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: elemId }
], () => {
done()
})
})

Actions can also do API requests that, when completed, should commit a mutation with the data received. Since the test should be as independent as possible, the API call should be mocked using a stub that should resolve or reject the Promise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[actionTypes.ACTION_SET_FAV_ELEM] (context) {
return APIAdapter.services.fetchFavElem()
.then((response) => {
context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, response)
resolve(true)
}).catch((error) => {
context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, undefined)
reject(error)
})
},

// the tests
it('should invoke mutation to set the favourite element if the API call is succcessful', done => {
const expectedPayload = 'AAAAA'
const fetchFavElemStub = sinon.stub(APIAdapter.services, 'fetchFavElem').resolves(expectedPayload)

testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], null, {}, [
{ type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: expectedPayload }
], () => {
fetchFavElemStub.restore()
done()
})
})

it('should invoke mutation to set the favourite element as undefined if the API is not succcessful', done => {
const fetchFavElemStub = sinon.stub(APIAdapter.services, 'fetchFavElem').rejects()

testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], null, {}, [
{ type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: undefined }
], () => {
fetchFavElemStub.restore()
done()
})
})

Testing Mutations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// the mutation
[mutationTypes.MUTATION_SET_FAV_ELEM] (state, elemId) {
state.favElem = elemId
}

// the test
it('should set state.favElem', () => {
const state = {
favElem: ''
}

mutations[mutationTypes.MUTATION_SET_FAV_ELEM](state, 'AAA')

expect(state.favElem).to.equal('AAA')
}

Testing Getters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// the getter
getElemsByCategory: state => (category) => {
return state.elems.filter((el, index, arr) => el.category === category)
}

// the test
it('should return the elements that have Cat1 as category', done => {
const expected = [
{
'id': '1',
'category': 'Cat1'
},
{
'id': '2',
'category': 'Cat1'
}
]

const state = [
{
'id': '1',
'category': 'Cat1'
},
{
'id': '2',
'category': 'Cat1'
},
{
'id': '3',
'category': 'Cat2'
}
]

expect(getters.getElemsByCategory(state)('Cat1')).to.be.deep.equal(expected)

done()
})