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
- 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).
- 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.
- 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.
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:
- 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). Adding this amount of control will add more code to the test (mocks, stubs, etc, must be created). Code coverage can be influenced by these behaviors as code that is executed on the lifecycle hooks will be marked as covered once it’s executed as the instance is created. The component’s instance behavior will replicate exactly the component’s behavior on the application.
- 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. Less code is produced to execute the test and code coverage will be more realistic. The component’s behavior can’t be completely reproduced (and it’s not intended to) as the tests will focus on the object’s functions and properties actions. Avoids some annoying errors (as an example, render errors) from underlying dependencies as the instance is not created.
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:
- Actions: What mutations are called from the action and what’s the payload? (the testAction function will be used for this purpose)
- Mutations: Was the state changed the way it was expected to?
- Getters: Is the getter retrieving the data correctly?
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>
context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, elemId)<br>
}<br>
// the test<br>
it('should invoke mutation to set the favourite element', done => {<br>
const elemId = 'AAAAAA'<br>
testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], elemId, {}, [<br>
{ type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: elemId }<br>
], () => {<br>
done()<br>
})<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>
return APIAdapter.services.fetchFavElem()<br>
.then((response) => {<br>
context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, response)<br>
resolve(true)<br>
}).catch((error) => {<br>
context.commit(mutationTypes.MUTATION_SET_FAV_ELEM, undefined)<br>
reject(error)<br>
})<br>
},<br>
// the tests<br>
it('should invoke mutation to set the favourite element if the API call is succcessful', done => {<br>
const expectedPayload = 'AAAAA'<br>
const fetchFavElemStub = sinon.stub(APIAdapter.services, 'fetchFavElem').resolves(expectedPayload)<br>
testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], null, {}, [<br>
{ type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: expectedPayload }<br>
], () => {<br>
fetchFavElemStub.restore()<br>
done()<br>
})<br>
})<br>
it('should invoke mutation to set the favourite element as undefined if the API is not succcessful', done => {<br>
const fetchFavElemStub = sinon.stub(APIAdapter.services, 'fetchFavElem').rejects()<br>
testAction(actions[actionTypes.ACTION_SET_FAV_ELEM], null, {}, [<br>
{ type: mutationTypes.MUTATION_SET_FAV_ELEM, payload: undefined }<br>
], () => {<br>
fetchFavElemStub.restore()<br>
done()<br>
})<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>
state.favElem = elemId <br>
}<br>
// the test<br>
it('should set state.favElem', () => {<br>
const state = {<br>
favElem: ''<br>
}<br>
mutations[mutationTypes.MUTATION_SET_FAV_ELEM](state, 'AAA')<br>
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:
- Avoriaz: Vue testing utility library with a rich API and great enhancements.
- Vue-test-utils: Vue official test util library (contains multiple contributions from the same author of avoriaz). The documentation has great tips on how to approach unit testing.
- Using Jest in Vue unit tests: If you want to change your unit testing tool to Jest, check these articles (also contains great tips and explains how to approach many test scenarios).
- @EddYerburgh: Author of avoriaz and owner of multiple repositories with great test code examples. Also, you can follow his blog!
- Sinon-chai: Adds multiple useful assertions to the test stack.