How to write tests for Vue
Let us continue the trend of making sure our applications still work and continue working for the rest of 2021. Last week we talked about how to write tests for your JS app. Let's extend that to modern framework.
Most of people are probably writing their apps in a modern library or framework like React, Vue, or Angular. These frameworks help you create your UI and functionalities faster and more maintainable in components. Think of them as units or blocks that make up your whole UI. Depending on how you organize your components, you can have buttons, inputs, and some other simple ones that standardize on the same styles or more complicated ones like date picker, search result list and its items that you can use on different places with the same functionalities.
What differs between these UI components and JavaScript functions is just that UI component has, well, UI portion. At the end of the day it will be rendered and show HTML on the page for users to interact with. This opens up different ways of testing and not only limited to just testing if the JavaScript function works.
I mainly use Vue these days so this article will focus on Vue. However the method and concept should be similar in other frameworks with slight differences in what tools to use and maybe best practices. The sample codes and file setup are inheriting from how to write tests for your JS app.
Preparation
No matter which testing library to use, we will need to have the following modules installed.
# npm
npm install vue vue-template-compiler
# yarn
yarn add vue vue-template-compiler
We also need some other libraries so Vue files and the corresponding JS files can be transpiled correctly during test.
# npm
npm install babel-jest vue-jest@4.0.0-beta.2 jest-transform-stub @babel/core
# yarn
yarn add babel-jest vue-jest@4.0.0-beta.2 jest-transform-stub @babel/core
Setup
It is relatively easy to set up before writing your tests. We need to update jest.config.js so jest can know what to do with Vue files, particularly moduleFileExtensions
and tranform
props. I've also added setupFiles
prop to include a file that jest will always run first when running tests.
// jest.config.js
module.exports = {
// Automatically clear mock calls and instances between every test
clearMocks: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: [
'<rootDir>/src/**/*.{js,vue}',
],
// The glob patterns Jest uses to detect test files
testMatch: ["**/test/**/*.spec.js"],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
testPathIgnorePatterns: ["/node_modules/"],
// The paths to modules that run some code to configure or set up the testing environment before each test
setupFiles: ['<rootDir>/jest.init.js'],
**// An array of file extensions your modules use
moduleFileExtensions: ["js", "vue"],
// A map from regular expressions to paths to transformers
transform: {
'.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest',
'.*\\.(css|scss)$': '<rootDir>/node_modules/jest-transform-stub',
},**
};
// jest.init.js
require('regenerator-runtime/runtime');
jest.init.js is useful if you need to define your own mock or global import something before tests run. In this case we include the polyfill that allows async await.
Vue Test Utils
Vue test utils is one of the most common libraries to use for testing Vue application. It provides some methods like mount
to mount your components and interact with them.
Install
# npm
npm install @vue/test-utils vue-jest -D
# yarn
yarn add @vue/test-utils vue-jest -D
Sample
Imagine we have a Vue component like:
<template>
<div class="item">
<span>Count: {{ count }}</span>
<button @click="incrementCount">Increment Count</button>
</div>
</template>
<script>
export default {
name: "Counter",
data() {
return {
count: 0,
};
},
methods: {
incrementCount() {
this.count += 1;
},
},
};
</script>
It's a simple counter that increments the count when the button is clicked. Let's test if the count on UI increments when the button is clicked.
import { mount } from "@vue/test-utils";
import Counter from "../src/Counter.vue";
describe("Counter Component Test Utils", () => {
it("should increment counter when click the button", async () => {
const wrapper = mount(Counter);
const button = wrapper.find("button");
await button.trigger("click");
expect(wrapper.find("span").text()).toContain("1");
});
});
You can know more about vue test utils from their documentation.
Testing Library
Testing library is another library dedicated to testing UI components. The concept and initiative is a bit different than what vue test utils allows you to do.
The more your tests resemble the way your software is used, the more confidence they can give you.
We write these unit tests to make sure our code works. In JavaScript, "code works" means the function returns the correct output given the input. In component, it usually means if the UI renders correctly with the correct states. If we can test and ensure UI renders fine, we have confidence that our code works. With that, Testing Library is more focusing on how we can write tests for the UI rather than the implementation detail. This way our tests are more maintainable as they don't get into too much detail but still test enough to give us confidence.
This doesn't mean you can only use Testing Library to achieve this principle. We see that it is possible with vue test utils in the test example. Testing Library just provides more methods that make it easier so at the end of the day it is a matter of preference. You can know more about it from their documentation.
Install
# npm
npm install @testing-library/jest-dom @testing-library/vue -D
# yarn
yarn add @testing-library/jest-dom @testing-library/vue -D
Sample
Using the same count sample, our test becomes:
import { render, fireEvent } from "@testing-library/vue";
import Counter from "../src/Counter.vue";
describe("Counter Component Testing Library", () => {
it("should increment counter when click the button", async () => {
const { getByText } = render(Counter);
const button = getByText("Increment Count");
await fireEvent.click(button);
getByText("Count: 1");
});
});
If we run the test with coverage on, we would get:
Conclusion
I personally like the way Testing Library proposes but I did need some time to work my mind around the idea. During that stage I found it was inconvenient to not have a Vue instance I could play with but now I feel it's much easier once I realize I am only testing the UI. Both of these libraries support vue router and vuex during the test. So at the end, it comes down to your preference.
Are you writing tests or have tests helped you prevent disaster? Tweet to me to let me know!