Unit testing with Vue — Approach, tips, and tricks — Part 1

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.

Basic notions

Applying a stub or a spy depends on the test’s context. It relies on how much control is needed to make the testing unit of code isolated from the application’s scope but still achieve the test’s objectives.

You can also find useful resources, people to follow and test libraries on the end of this article.

Testing methodologies

Writing tests require the developer to understand how the code really works. To understand the deepness of Vue unit testing, it’s required to know what a Vue component instance means and how we can deal with a component’s instance.

Vue components’ attributes are all functions. A component’s beforeMount, mounted (…) are just functions like any other and can be tested like so. When we aim to test a component’s instance, we’re aiming to test Object properties and methods. There’s no black box here, just a behavioral abstraction created by our engine, Vue.

With that in mind, we can approach Vue unit testing in two major ways:

Each methodology has some pros and cons and it’s up to each team or developer to choose. It’s easier to find references to instance oriented tests (as you can see on avoriaz documentation or Vue documentation) but with this article, examples of both approaches will be shown to you!

Store tests

One of the main elements of a Vue web-app is the store. The store has three main structures that should be tested. Each can focus on a question that the test should answer to:

Store tests are probably the easiest tests to develop as the environment is highly isolated from the rest of the application (no child components, rendering issues, etc). 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:

-- CODE language-js line-numbers--
// the action <br>
[actionTypes.ACTION_SET_FAV_ELEM] (context, elemId) {<br>
 &nbsp;context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, elemId)<br>
}<br>
// the test<br>
it('should invoke mutation to set the favourite element', done => {<br>
 &nbsp;const elemId = 'AAAAAA'<br>
 &nbsp;testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], elemId, {}, [<br>
  &nbsp;&nbsp; { type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: elemId }<br>
&nbsp; ], () => {<br>
  &nbsp;&nbsp; done()<br>
 &nbsp;})<br>
})<br>

Using testAction function allows testing if all the expected mutations are invoked with the right payload.

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.

-- CODE language-js line-numbers--
// the action<br>
[actionTypes.ACTION_SET_FAV_ELEM] (context) {<br>
  &nbsp;return APIAdapter.services.fetchFavElem()<br>
    &nbsp; &nbsp;.then((response) => {<br>
     &nbsp; &nbsp; &nbsp; context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, response)<br>
     &nbsp; &nbsp; &nbsp; resolve(true)<br>
    &nbsp; &nbsp;}).catch((error) => {<br>
     &nbsp; &nbsp; &nbsp; context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, undefined)<br>
     &nbsp; &nbsp; &nbsp; reject(error)<br>
    &nbsp; &nbsp;})<br>
 },<br>
// the tests<br>
it('should invoke mutation to set the favourite element if the API call is succcessful', done => {<br>
  &nbsp;const expectedPayload = 'AAAAA'<br>
  &nbsp;const fetchFavElemStub = sinon.stub(APIAdapter.services, 'fetchFavElem').resolves(expectedPayload)<br>
 &nbsp;testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], null, {}, [<br>
   &nbsp; &nbsp; { type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: expectedPayload }<br>
  &nbsp;], () => {<br>
   &nbsp; &nbsp; fetchFavElemStub.restore()<br>
    &nbsp; &nbsp;done()<br>
 &nbsp; })<br>
})<br>
it('should invoke mutation to set the favourite element as undefined if the API is not succcessful', done => {<br>
  &nbsp;const fetchFavElemStub = sinon.stub(APIAdapter.services, 'fetchFavElem').rejects()<br>
 &nbsp; testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], null, {}, [<br>
  &nbsp; &nbsp;  { type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: undefined }<br>
  &nbsp;], () => {<br>
   &nbsp; &nbsp; fetchFavElemStub.restore()<br>
   &nbsp; &nbsp; done()<br>
  &nbsp;})<br>
})<br>

In the above example, using the stub allowed us to test the success and the failure cases of the API request. Different payloads are passed to the mutation depending on the Promise result.

The testAction function can also receive an object with the current state for scenarios where the mutation payload depends on a manipulation of the current state (using the context.state).

Testing mutations

-- CODE language-js line-numbers--
// the mutation<br>
[mutationTypes.MUTATION_SET_FAV_ELEM] (state, elemId) {<br>
 &nbsp;state.favElem = elemId <br>
}<br>
// the test<br>
it('should set state.favElem', () => {<br>
&nbsp; const state = {<br>
 &nbsp;&nbsp;  favElem: ''<br>
 &nbsp;}<br>
&nbsp;mutations[mutationTypes.MUTATION_SET_FAV_ELEM](state, 'AAA')<br>
&nbsp;expect(state.favElem).to.equal('AAA')<br>
}

On mutations like the example above, the test should consist on assuring that the state is changed as expected. The initial state is passed to the mutation as context and the expect statement will compare the current state (after the mutation execution) with the expected value.

Even in more complex scenarios, the approach remains similar.

Testing getters

Getters are functions that use the store to do some kind of operation (filter, count, …). A return value will always exist and the test should consist of checking if that return value is correct in relation to the context — the current state. As an example, check the code below where a category is received as a parameter on the getter in order to filter a list of elements.

Resources

Some resources that might help you on Vue unit tests:

Bruno Teixeira
Head of Product