This book is written for Vue.js 3 and Vue Test Utils v2.
Find the Vue.js 2 version here.
# Installing vue-cli
vue-test-utils
is the official testing library for Vue, and will be used throughout the guide. It runs in both a browser and Node.js environment, and works with any test runner. We will be running our tests in a Node.js environment throughout this guide.
vue-cli
is the easiest way to get started. It will set up a project, as well as configure Jest, a popular testing framework. Install it by running:
yarn global add @vue/cli
or with npm:
npm install -g @vue/cli
Create a new project by running vue create [project-name]
. Choose "Manually select features" and "Unit Testing", and "Jest" for the test runner.
Once the installation finishes, cd
into the project and run yarn test:unit
. If everything went well, you should see:
PASS tests/unit/HelloWorld.spec.js
HelloWorld.vue
✓ renders props.msg when passed (26ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.074s
Congratulations, you just ran your first passing test!
# Writing your first test
We ran an existing test that came with the project. Let's get our hands dirty, writing our own component, and a test. Traditionally when doing TDD, you write the failing test first, then implement the code which allows the test to pass. For now, we will write the component first.
We don't need src/components/HelloWorld.vue
or tests/unit/HelloWorld.spec.js
anymore, so you can delete those.
# Creating the Greeting
component
Create a Greeting.vue
file in src/components
. Inside Greeting.vue
, add the following:
<template>
<div>
{{ greeting }}
</div>
</template>
<script>
export default {
name: "Greeting",
data() {
return {
greeting: "Vue and TDD"
}
}
}
</script>
# Writing the test
Greeting
has only one responsibility - to render the greeting
value. The strategy will be:
- render the component with
mount
- assert that the component's text contains "Vue and TDD"
Create a Greeting.spec.js
inside tests/unit
. Inside, import Greeting.vue
, as well as mount
, and add the outline of the test:
import { mount } from '@vue/test-utils'
import Greeting from '@/components/Greeting.vue'
describe('Greeting.vue', () => {
it('renders a greeting', () => {
})
})
There are different syntaxes used for TDD, we will use the commonly seen describe
and it
syntax that comes with Jest. describe
generally outlines what the test is about, in this case Greeting.vue
. it
represents a single piece of responsibility that the subject of the test should fulfill. As we add more features to the component, we add more it
blocks.
Now we should render the component with mount
. The standard practice is to assign the component to a variable called wrapper
. We will also print the output, to make sure everything is running correctly:
const wrapper = mount(Greeting)
console.log(wrapper.html())
# Running the test
Run the test by typing yarn test:unit
into your terminal. Any file in the tests
directory ending with .spec.js
is automatically executed. If everything went well, you should see:
PASS tests/unit/Greeting.spec.js
Greeting.vue
✓ renders a greeting (27ms)
console.log tests/unit/Greeting.spec.js:7
<div>
Vue and TDD
</div>
We can see the markup is correct, and the test passes. The test is passing because there was no failure - this test can never fail, so it is not very useful yet. Even if we change Greeting.vue
and delete the greeting
from the template, it will still pass. Let's change that.
# Making assertions
We need to make an assertion to ensure the component is behaving correctly. We can do that using Jest's expect
API. It looks like this: expect(result).to [matcher] (actual)
.
Matchers are methods to compare values and objects. For example:
expect(1).toBe(1)
A full list of matchers is available in the Jest documentation. vue-test-utils
doesn't include any matchers - the ones Jest provides are more than enough. We want to compare the text from Greeting
. We could write:
expect(wrapper.html().includes("Vue and TDD")).toBe(true)
but vue-test-utils
has an even better way to get the markup - wrapper.text
. Let's finish the test off:
import { mount } from '@vue/test-utils'
import Greeting from '@/components/Greeting.vue'
describe('Greeting.vue', () => {
it('renders a greeting', () => {
const wrapper = mount(Greeting)
expect(wrapper.text()).toMatch("Vue and TDD")
})
})
We don't need the console.log
anymore, so you can delete that. Run the tests with yarn test:unit
, and if everything went well you should get:
PASS tests/unit/Greeting.spec.js
Greeting.vue
✓ renders a greeting (15ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.477s, estimated 2s
Looking good. But you should always see a test fail, then pass, to make sure it's really working. In traditional TDD, you would write the test before the actual implementation, see it fail, then use the failing errors to guide your code. Let's make sure this test is really working. Update Greeting.vue
:
<template>
<div>
{{ greeting }}
</div>
</template>
<script>
export default {
name: "Greeting",
data() {
return {
greeting: "Vue without TDD"
}
}
}
</script>
And now run the test with yarn test:unit
:
FAIL tests/unit/Greeting.spec.js
Greeting.vue
✕ renders a greeting (24ms)
● Greeting.vue › renders a greeting
expect(received).toMatch(expected)
Expected value to match:
"Vue and TDD"
Received:
"Vue without TDD"
6 | const wrapper = mount(Greeting)
7 |
> 8 | expect(wrapper.text()).toMatch("Vue and TDD")
| ^
9 | })
10 | })
11 |
at Object.<anonymous> (tests/unit/Greeting.spec.js:8:28)
Jest gives us good feedback. We can see the expected and actual result, as well as on which line the expectation failed. The test did fail, as expected. Revert Greeting.vue
and make sure the test is passing again.
# Using Vue 3 and the Composition API
Vue 3 adds another API for building components - the Composition API. One sign of a good test is we avoid testing implementation details (how the code works) but instead focus on behavior (what the code does). Let's refactor the above component and see what happens. If the test still passes, we know it's testing the right things. If it fails, we could be testing an implementation detail
<template>
<div>
{{ greeting }}
</div>
</template>
<script lang="ts">
export default {
name: 'Greeting',
setup() {
const greeting = 'Vue and TDD';
return {
greeting,
};
},
};
</script>
When starting with the Composition API you often forget to add the variable to the return. Try omitting this and see how the test fails. If you are planning to convert some of your Options API components the Composition API, some tests can give you confidence and provide a positive feedback loop during the refactor.
# Next
Next we will look at the two methods vue-test-utils
provides to render components: mount
and shallowMount
.