Software testing, huh? Some people hate writing tests, some people love it. Some argue what is the best coverage and some don’t think about the coverage at all. Some leave testing to the end of the development cycle and some start their development cycle by writing tests. In the end everyone agrees (at least I hope so), that every piece of software needs to be tested. If you have doubts about it, try imagining that you got on a plane which has a software that has never been tested. Not a pleasant thought, right? Surely you can say, I don’t write software for the planes, I am a front end developer that mostly creates websites or information systems for other companies. And you will be right, a mistake in you program may not cause the death of hundreds of people, but it still can lead to a huge loss of money for your client or your boss. And that is not good.
Testing is a vast theme to talk about and of course this post would be too long if i tried to describe every aspect and type of the software testing, so in this article I will show some basics of the unit and snapshot testing the Vue.js application. But first let’s talk about what are unit and snapshot tests at all?
Imagine you assembled a car. You spent a lot of time in your workshop, thinking about how your car should look or how every of its components should work together. And now here it is, standing in front of your house, waiting for you. You start the engine, but as soon as the car starts moving you feel that there is something wrong with it. It is not as controllable as you thought it would be, and it doesn’t speed up that well. You move your car back to your workshop and try to figure out what is wrong. You look under the hood first to see if everything is OK there, then you check your steering wheel and only after that you notice that your tire is punctured. What a misfortune! Such a simple problem, that could be fixed so easily. But you spent a lot of time trying to figure out where it was. If only you had a helper team of mechanics, that would quickly check every part of your car separately anytime you ask them. Well, we are getting to the point here, unit testing is something like this team of mechanics.
Unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.
In other words, you split your program into some logical, structural or functional pieces and test these pieces separately to check if all of them work exactly as they should. Since we are talking about Vue.js application, the unit is already defined for us, because Components are the core concept of the Vue framework.
Now let’s return back to your car. You love the way your car look, so right before you tried to take your car on its first ride, you had probably took a picture of it. And now after picking up new tires for your car, you look at the picture you took several hours ago and to your surprise you realize that your car doesn’t look that good any more. So the old picture of your car helps you to understand that something has changed in the way the car looks. Just like that the snapshot test will help you make the UI of your Vue.js component more consistent when you want it to be consistent. Sometimes fixing small mistake may lead to the component’s UI change that you’ve never wanted. That is the job for a snapshot test. When you run your snapshot test for the first time, it will create a snapshot file that contains a rendered version of your component. On the other runs it will render your component again and compare it to the stored version. If there are some differences between the stored version and the new one, the test will fail, making you aware that something probably went wrong during development.
So now when we have it covered, let’s talk about libraries that can help us test our application.
- Vue Test Utils is the official unit testing library for Vue.js framework. It has some useful methods for rendering, mounting, shallow mounting your components to test them. It also has an API to work with
the mounted components, get access to their inner properties, catch events, pass props and slots to the component, etc.
Setting up testing environment
Setting up Jest is not a hard task. First let’s install packages that we will need.
Then add the following code to your existent .babelrc file or create a new one.
After that is done, create a jest.config.js file in the root of your project. That is where you will configure Jest. I will not go through every possible configuration that Jest has, because there are quite a few of them. But you can find the list of all possible configuration options here.
And that is it, we can now create our first test.
Testing Vue component
As I have already mentioned before, components are the main units of the Vue application. There are two ways to test the code of your component: trying to test every computed property and method that your component has or test only “inputs” and “outputs” of your component. The second way is sometimes called black box testing as if the internal implementations of the component’s methods, properties and hooks were inside the black box and we couldn’t see them, so we can only test the public API of the component. I personally prefer the second option and I can explain you why. What if I tell you to test whether your smartphone can play music. What will your steps be? You will most likely find some music app, select a song and then listen to your phone’s speaker. At this moment you don’t have to take a screwdriver and open up your phone to check all of its circuits. You are just working with the inputs (selecting app and song) and outputs (listening to music) of your phone to test its ability to play music. It works the same with the Vue components. Sometimes the implementation of some method inside your component may change during development process without involving your component’s outputs or public API and in case you are using black box testing you wouldn’t have to rewrite your tests every time this happens.
Testing component’s UI with different props
First we will try testing different UI states of the component related to the change of the component’s inputs. Look at the TodoList component below.
It is a simple component that takes list of todos in the todos property and then renders all of them on the page. If the array of todos is empty, the component will render a short message about it. The only input this component has is todos property and the only output is component’s UI. We don’t need to know the internal implementation of this logic while testing it. So it is a perfect candidate for the black box testing and that is what we are going to do.
To test this kind of components we need to change the value of its properties and check if the UI has changed as we expected.
Let’s go through the code line by line to understand what is happening:
- describe(name, fn) is the Jest Framework built-in function used to create a block of the related tests. In our case we are using it to create a block for all the TodoList tests.
- it(name, fn, timeout) (the alias of test(name, fn, timeout)) is used to create a test. The first argument is the name of the test which usually start with the word should to show what is expected to happen.
- shallowMount(component, config) allows us to mount the component without rendering its child components and return the Wrapper instance. All of the child components will be stubbed. Vue Test Utils also has another function named mount(component, config) that mounts component with all its child components without stubbing them.
- expect(value) allows us to check that the values meet certain conditions. It is a pretty complex API suitable for many situations. You can read more about it here
- expect(wrapper.element).toMatchSnapshot() while run for the first time would generate a snapshot that will be saved to the TodoList.spec.js.snap file in the __snapshots__ folder. Changing “You have nothing else to do” text to something else and running your test will lead to the 1 snapshot failed error.
This test could also be written without snapshots. It could be done by replacing the toMatchSnapshot() lines with another expect API calls checking that the ul list is not present on the page during the first test, and that ul list has 1 item during the second test. TodoList component is not a complex one and the snapshot test here shows exactly what are we testing. But the component that you are testing is in fact complex or it is not easy to see what are you actually testing by looking at the snapshot than consider using assertion tests.
Testing user interactions and events
Here we have another example of the component.
It doesn’t have any props as the previous component had, and the UI of this component does not change. Maybe we don’t need to write test for this one? The truth is we do. The AddTodoForm component may not have props, but it still has an input — user interaction. The UI of this component may not change, but the component still has an output — event. So in order to test a component like that, we need to simulate user actions and check if the event has been triggered.
Once again, let’s look at the lines that we haven’t seen before:
- jest.fn() is mocking API from the Jest framework, that allows you to control mocked functions flow and track how many times it has been called or which arguments has been passed. You can read more about mocking in the Jest framework here.
- wrapper.vm.$on(eventType, handler) is used to set a handler to the event triggered by our component.
- wrapper.find(selector) is used to find a child element of our component by its CSS selector. It returns instance of a Wrapper.
- wrapper.trigger(eventType, options) is used to trigger an event on the wrapper. In this particular case it is used to simulate click event.
- expect(spy).toHaveBeenCalledTimes(n) is used to assert that the spy has been called exactly n times.
- expect(spy).toHaveBeenCalledWith(…args) is used to assert that the spy has been called with …args.
Surely, these were just examples of how you could test your components. In real world projects you would probably need to test more complex components and you would meet some mixed versions of this tests. For example user action can trigger UI changes or different props result in different events that component triggers once user interacts with it.
Testing Vuex store
Testing Vuex getters
First let’s look at the getters as they are the most simple of all. As mentioned above getter is function that takes current state, processes it without mutating, caches the result and then returns the value. You can see an example of the getConfirmedUsers getter below. It takes users from the state, filters out all unconfirmed users and return an array of the confirmed users.
To test this getter we will need to call it with mocked inputs and then compare the returned value to the value that is expected. We start by creating the mocked state and the expected result of the operation. Then we use the expect API to make sure that the returned value of the getConfirmedUsers getter call equals the expected result.
Testing Vuex mutations
Here we have an example of the mutation that takes an array of items (all of which have some category), groups them by their categories and saves them to the store.
The main mutation testing method is passing arguments to the mutation and then checking if the state has been set properly. So that is exactly what we are going to do. We create a mocked state and a value to mutate it, call the mutation with these arguments and use the expect API to check whether the state has been mutated as we expected it to be.
Testing Vuex actions
Actions are usually more complex than getters and setters, because they can contain some asynchronous code (HTTP calls, Promise, etc.) or usages of the external libraries that have to be mocked before running tests. Fortunately Jest got it covered for us. Below you can see a simple example of the Vuex action. It makes an API call using register function and then if the call has been successful it commits a mutation to mutate the state.
First of all we will need to mock an ./api/auth module to make sure that it doesn’t call an actual API during our tests. Then we will spy on the commit function to check if the mutation has been committed.
As I have said earlier Jest has a good API for the mocking, so mocking a module should not be a problem. We do this by calling jest.mock(moduleName) function, that will create a mock for the module. Then we create a spy for the commit function to be able to track whether it has been called. After that we use mockResolvedValue(value) method to change the inner implementation of the register function with the return Promise.resolve(value).
Can I change components props without fully remounting it?
Yes, you can. shallowMount(component, config) and mount(component, config) methods return an instance of the Wrapper class that has some very useful methods that will help you during testing process. As for the props, Wrapper instance has the setProps(props) method that allows you to pass new props to the mounted component.
setProps(props) is not the only useful method and property there. You can search for child nodes inside your mounted components using find(selector) or findAll(selector), or even replace mounted
component’s methods with spies using setMethods(methods). You can see the list of all Wrapper methods here.
What if I use some globally installed third-party components (such as Element.io)?
You can create a local instance of Vue.js right inside of your test file using the createLocalVue() method from the @vue/test-utils package. You must then pass this instance to the localVue property of the configuration object of shallowMount or mount method. You can read more about local Vue instances here.
What if I am using some globally installed service (such as $t from vue-i18n, $router from vue-router)?
You can mock services like that by using the mocks property of the configuration object of shallowMount or mocks method.
- You can read more about mocks here.
What if my component is connected to the Vuex store?
You can mock $store using the mocks property as in the example above or you can use createLocalVue() method from the @vue/test-utils to create the local instance of Vue and connect Vuex to it. This way you will have a real instance of store inside your tests.
How to see code coverage?
Code coverage can be enabled either by passing — coverage argument to the Jest CLI or by setting collectCoverage property to true in the jest.config.js file. You can also specify which files must be used to collect coverage by setting collectCoverageFrom property to an array of paths.
Some of the basic takeaways of this article:
- Do not reinvent the wheel. There are a lot of tools that will make your life simpler.
- While testing a Vue.js component, try focusing on its public API. Remember that the implementation may change and your tests shouldn’t break every time this happens.
- Use snapshot testing if the UI of your component is not very complex and when another developer can see exactly what are you testing by looking at your snapshot.
- Vuex store is a great testing candidate too, if a lot of logic in your application rely on it.
In the end all I wanted to tell you is that testing is not hard and tests are worth spending time on, if you want to make your code more maintainable or error-protected.