Vue.js 3 Unit Testing Setup & Basics Tutorial
In this Vue tutorial we learn about Unit testing.
We cover how to set up a testing environment and the basics of writing and running tests with the Jest test runner.
- Lesson Video
- Lesson Project
- What is Unit Testing?
- File structure and naming
- How to write a test with the test method
- How to run a test
- How to write a test with the it method
- How to group tests into a test suite with the describe method
- How to skip a test with the xit method
- Test failures
- Sanity test
- Further Reading
Lesson Video
If you prefer to learn visually, you can watch this lesson in video format.
Lesson Project
If you want to follow along with the examples, you will need to create an app generated by the Vue CLI with Unit Testing added.
We want to Manually select features.
? Please pick a preset:
Default ([Vue 2] babel, eslint)
Default (Vue 3) ([Vue 3] babel, eslint)
> Manually select features
Then add the Unit Testing.
? Check the features needed for your project:
(*) Choose Vue version
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
( ) Router
( ) Vuex
( ) CSS Pre-processors
(*) Linter / Formatter
>(*) Unit Testing
( ) E2E Testing
Then select Jest as the testing solution.
? Pick a unit testing solution:
Mocha + Chai
> Jest
The CLI will install the following tools.
- Jest is the framework used to perform unit testing.
- Vue CLI Unit Jest Plugin installs and configures the necessary modules for Jest to work in our project.
- Vue-Jest is a transformer to compile our code to files that Jest will understand before the tests run.
- Vue Testing Utilities is a set of utility functions that help us test Vue applications.
Each of them can be installed manually into an existing project. To keep things simple for now, we’ll let the CLI do the setup and configuration for us.
What is Unit Testing?
Unit testing checks if smaller sections of our application, like functions or components, work as expected. Unit tests are faster and more reliable.
As an example, let’s consider a shopping app’s basket component. The component needs to calculate the total of all the products in the basket and display it to the user.
The most likely place for it to fail is in the function that calculates the total, so we can test if the function displays the correct output. Such a Unit test might take the following steps.
- Pass two values to the function, for example 1 and 1.
- Check that the result is 2 and throw an error if it’s not.
- End the test.
Unit testing is fast because we can immediately test a small unit of code after making changes. It also helps developers working in teams to quickly understand code they didn’t write.
That said, unit testing does have its own disadvantages.
- Unit tests make it unappealing to refactor your code. Even smaller changes like splitting a function will require you to rewrite your tests.
- We can only test if individual sections of code work, we can’t test if multiple sections work together.
File structure and naming
The CLI will create a few new files and folders for us, let’s quickly explore and explain them.
Test file structure
Although we can store test files anywhere in the project, the convention in Vue is to store them in /tests/unit/ in the project’s main directory.
project-name/
├── public/
├── src/
|
├── tests/
| ├── unit/
| | └── example.spec.js
|
└── jest.config.js
The CLI has created an example test for us in /tests/unit/example.spec.js that should look similar to the following.
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
props: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})
We’re going to write our own test from scratch in a little bit so it’s safe to delete the code in this file.
The jest.config.js file tells Vue to use the Vue-Jest tool as a transformer for our .vue files.
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
transform: {
'^.+\\.vue$': 'vue-jest'
}
}
We don’t need to change anything in this config at the moment.
Test file naming
How we name our test files is also very important. Jest (and other frameworks) use file names to find the tests.
A file can be named whatever we want, as long as it includes either .spec , or .test before the file extension.
name.spec.js // Javascript
name.spec.ts // TypeScript
// or
name.test.js
name.test.ts
The convention in Vue is to use .spec and give the file the same name as the component it’s testing.
For example, if we had a component called HelloWorld , we would name its test file as one of the following.
HelloWorld.spec.js
HelloWorld.spec.ts
How to write a test with the test method
Jest provides us with the test method to write our unit tests.
The test method takes two arguments.
- A string outline or description of the test being done.
- A callback that contains the test assertion(s).
test('Description', () => {
// assertion (what we want
// the test to check for)
})
Let’s see an example. Create a file called example.spec.js in your project’s /tests/unit/ folder. If one already exists from the project generation, delete the contents.
We’ll start with the outline and describe what we want the test to accomplish.
In our case, we want to do something simple, like check if 1 + 1 = 2, so we can write that.
test('Expect 1 + 1 = 2', () => {
})
The next step is to create an assertion with Jest’s expect API. The expect API uses matchers to compare values and objects.
expect(something).to[matcher](value)
For example, if we want to test for exact equality, we would use the toBe matcher.
expect(something).toBe(value)
note The Vue Test Utilities doesn’t provide any matchers. The ones Jest makes available is more than enough to test anything we need.
We’ll use the toBe matcher in our assertion because we know what the exact value of the result should be.
test('Expect 1 + 1 = 2', () => {
// expect the result
// of 1+1 to be 2
expect(1+1).toBe(2)
})
How to run a test
When the project generated, it created a new testing script in the package.json file.
"test:unit": "vue-cli-service test:unit",
Executing the command in the terminal will tell Jest to look for any files in our project with .spec or .test , and run them all.
So to test the file we wrote above, all we need to do is run the following command.
npm run test:unit
Jest will run the tests and output a message that shows if the they have passed or failed.
PASS tests/unit/example.spec.js
√ Expect 1 + 1 = 2 (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.225s
Ran all test suites.
The first line shows if the test failed or passed, and the test file’s location in our project. The second line shows our assertion description and how long it took to run. Everything below that is the test metadata.
How to write a test with the it method
Because Jest is built on top of Jasmine, it has access to Jasmine’s describe it syntax. That means we can use the it method instead of test to write our tests.
They work the same, test is just Jest’s implementation of it. So we don’t need to do anything special here, just replace test with it and everything will still work as expected.
it('Expect 1 + 1 = 2', () => {
// expect the result
// of 1+1 to be 2
expect(1+1).toBe(2)
})
It doesn’t matter which method you use, as long as you stay consistent.
How to group tests into a test suite with the describe method
The describe it syntax allows us to group our tests by wrapping them with the describe method.
describe('The car', () => {
it('is blue', () => {
expect(color).toBe(blue)
}),
it('is fast', () => {
expect(topSpeed).toBe(300)
}),
it('is safe', () => {
expect(ncap).toBe(5)
})
})
Because all the tests are about the car, it makes sense to group them. Similarly, we can group a component’s tests.
Because we’re working with Jest, we can use the test method instead of it .
describe('The car', () => {
test('is blue', () => {
expect(color).toBe(blue)
}),
test('is fast', () => {
expect(topSpeed).toBe(300)
}),
test('is safe', () => {
expect(ncap).toBe(5)
})
})
Again, it doesn’t matter which you use, just stay consistent.
Let’s see it in action by modifying our earlier test to be grouped with describe .
We’ll also add a second test, separating the two with a comma. The second test chains a not matcher to the expectation that works just like a regular conditional != operator.
describe('Example', () => {
it('1 + 1 = 2', () => {
expect(1+1).toBe(2)
}),
it('1 + 1 != 3', () => {
expect(1+1).not.toBe(3)
})
})
If we run the test, we should see the following output.
PASS tests/unit/example.spec.js
Example
√ 1 + 1 = 2 (2ms)
√ 1 + 1 != 3 (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.308s
Ran all test suites.
This time we can see the meta data shows 2 tests but still 1 test suite. When we group our tests with describe, they count as a single test suite.
In Vue, the convention is to describe our test suite with the component name. For example, if we had a component called HelloWorld.vue , we would describe its test suite as follows.
describe('HelloWorld.vue', () => {
it('Individual test description', () => {
// assertion
})
})
How to skip a test with the xit method
We can skip a test by prefixing the it method with an x.
xit('Description', () => {
// test will be skipped
})
Technically it’s a different method, but makes it quick and convenient to skip a test.
As an example, let’s skip the second test in our test suite.
describe('Example', () => {
it('1 + 1 = 2', () => {
expect(1+1).toBe(2)
}),
xit('1 + 1 != 3', () => {
expect(1+1).not.toBe(3)
})
})
Running the test will produce the following output.
PASS tests/unit/example.spec.js
Example
√ 1 + 1 = 2 (5ms)
○ skipped 1 + 1 != 3
Test Suites: 1 passed, 1 total
Tests: 1 skipped, 1 passed, 2 total
Snapshots: 0 total
Time: 4.196s, estimated 14s
Ran all test suites.
This time we can see the second test was skipped from the assertion and the metadata.
Test failures
In Test Driven Development (TDD), we write a failing test before we try to write a passing one. We aren’t going to discuss TDD here. All we want from this approach right now, is knowing that the test works, which a failing test shows us.
When a test fails, Jest will do its best to tell us what went wrong and where.
As an example, let’s change our expectation to be the wrong value.
test('Expect failure', () => {
// will fail
expect(1+1).toBe(3)
})
Of course 1 + 1 doesn’t equal 3 so the test will fail. Let’s see what that looks like.
FAIL tests/unit/example.spec.js
× Expect failure (6ms)
● Expect failure
expect(received).toBe(expected) // Object.is equality
Expected: 3
Received: 2
2 |
3 | // will fail
> 4 | expect(1+1).toBe(3)
| ^
5 | })
6 |
at Object.<anonymous> (tests/unit/example.spec.js:4:15)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.253s
Ran all test suites.
The output shows all the same things we get with a passing test, but it also includes the assertion error, the expected and received values, and on which line and column the failure is likely to come from.
So from the output, we know the following.
- The file that failed.
- The matcher used.
- The expected and received values.
- The line number and column the failure is likely to come from.
All we have to do to fix it, is go to the line and ensure the received value (or behavior) is the same as the expected value (or behavior).
In our case that’s as simple as changing the 3 back into a 2, and the test will pass again.
Sanity test
In some cases a test can fail because the tools we’re using isn’t working correctly. A sanity test is a simple test that should always pass to check that the tools work correctly. If the test doesn’t pass, we’ll know that the problem isn’t with our codebase.
tip It’s recommended to always write a sanity test as the first one in our suite.
We already know our tools work because we’ve been testing throughout the lesson. But to demonstrate, let’s change our example test to something that should always pass.
test('Sanity test', () => {
expect(true).toBe(true)
})
We’ll keep this test in our suite throughout the testing process. If it fails at any time, we’ll know that something went wrong with the tools and it’s not our codebase that’s the problem.
Further Reading
For more information on the topics covered in this lesson, please see the relevant sections below.