Vue.js Jest Setup
Introduction
Testing is a critical part of developing robust applications, and Vue.js offers excellent integration with Jest, a popular JavaScript testing framework. In this tutorial, you'll learn how to set up Jest for Vue.js applications, configure it properly, and start writing your first tests.
Jest provides features like snapshots, coverage reporting, and mocking, making it a powerful tool for Vue.js developers. Whether you're working on a small project or an enterprise application, proper test setup will help ensure your code remains maintainable and reliable.
Prerequisites
Before we begin, make sure you have:
- Node.js (version 12 or later) installed
- A basic Vue.js project (Vue CLI or Vite based)
- Basic understanding of Vue.js components
Installing Jest for Vue.js
Option 1: Using Vue CLI
If you're using Vue CLI to create your project, you can add the Jest testing framework during project creation by selecting the "Unit Testing" option and choosing "Jest".
For existing Vue CLI projects, you can add Jest using the Vue CLI:
vue add unit-jest
This command installs all necessary dependencies and configures your project for Jest testing.
Option 2: Manual Installation
For projects not using Vue CLI, you'll need to install the necessary packages manually:
npm install --save-dev jest @vue/test-utils vue-jest @vue/vue3-jest babel-jest
For Vue 2.x projects, use this configuration instead:
npm install --save-dev jest @vue/test-utils@1 vue-jest@4 babel-jest
Configuring Jest
After installation, you need to configure Jest for Vue.js. Create a jest.config.js
file in your project root:
For Vue 3:
module.exports = {
testEnvironment: "jsdom",
transform: {
"^.+\\.vue$": "@vue/vue3-jest",
"^.+\\.jsx?$": "babel-jest",
"^.+\\.js$": "babel-jest",
},
moduleFileExtensions: ["vue", "js", "json", "jsx"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"
},
testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.{js,vue}",
"!src/main.js",
"!src/App.vue",
"!**/node_modules/**"
]
};
For Vue 2:
module.exports = {
testEnvironment: "jsdom",
transform: {
"^.+\\.vue$": "vue-jest",
"^.+\\.jsx?$": "babel-jest",
"^.+\\.js$": "babel-jest",
},
moduleFileExtensions: ["vue", "js", "json", "jsx"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"
},
testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.{js,vue}",
"!src/main.js",
"!src/App.vue",
"!**/node_modules/**"
]
};
Configuration Explained
Let's break down what each part of the configuration does:
testEnvironment
: Sets the testing environment to jsdom which simulates a browser environmenttransform
: Defines how different file types should be processedmoduleFileExtensions
: File types Jest should look for and processmoduleNameMapper
: Maps import paths (like@/components
) to directory locationstestMatch
: Patterns to match test filescollectCoverage
: Enables test coverage reportingcollectCoverageFrom
: Specifies which files to include in coverage reports
Babel Configuration
Jest requires Babel to transform modern JavaScript. Create or update your babel.config.js
file:
module.exports = {
presets: [
'@babel/preset-env'
]
};
Adding Test Scripts
Add Jest test scripts to your package.json
file:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
These scripts allow you to:
- Run tests once with
npm test
- Run tests in watch mode with
npm run test:watch
- Generate coverage reports with
npm run test:coverage
Project Structure for Tests
There are two common approaches to organizing your tests:
1. Tests alongside components
src/
├── components/
│ ├── Button.vue
│ └── __tests__/
│ └── Button.spec.js
├── views/
│ ├── Home.vue
│ └── __tests__/
│ └── Home.spec.js
2. Separate tests directory
├── src/
│ ├── components/
│ │ └── Button.vue
│ └── views/
│ └── Home.vue
├── tests/
│ └── unit/
│ ├── Button.spec.js
│ └── Home.spec.js
The first approach keeps tests closer to the implementation, while the second approach keeps test files more organized. Choose the one that works best for your team.
Writing Your First Vue.js Test
Let's create a simple component and test it. First, the component:
<!-- src/components/Counter.vue -->
<template>
<div>
<p data-testid="count">Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count += 1
},
decrement() {
this.count -= 1
}
}
}
</script>
Now let's write a test for this component:
// src/components/__tests__/Counter.spec.js
import { mount } from '@vue/test-utils'
import Counter from '../Counter.vue'
describe('Counter.vue', () => {
it('renders initial count', () => {
const wrapper = mount(Counter)
const countText = wrapper.find('[data-testid="count"]').text()
expect(countText).toContain('Count: 0')
})
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button:first-child').trigger('click')
const countText = wrapper.find('[data-testid="count"]').text()
expect(countText).toContain('Count: 1')
})
it('decrements count when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button:nth-child(2)').trigger('click')
const countText = wrapper.find('[data-testid="count"]').text()
expect(countText).toContain('Count: -1')
})
})
Test Breakdown
- We import
mount
from Vue Test Utils to create a wrapper around our component - We create test cases using Jest's
describe
andit
functions - We interact with the component using Vue Test Utils methods:
find()
to select elementstext()
to get text contenttrigger()
to simulate events
Running Tests
Now run your tests using the command:
npm test
You should see output similar to this:
PASS src/components/__tests__/Counter.spec.js
Counter.vue
✓ renders initial count (15ms)
✓ increments count when button is clicked (5ms)
✓ decrements count when button is clicked (4ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 2.5s
Adding Snapshot Tests
Snapshot tests are a powerful feature of Jest that helps you ensure your UI doesn't change unexpectedly. Let's add a snapshot test for our Counter component:
// src/components/__tests__/Counter.spec.js
// Add this test to your existing suite
it('matches snapshot', () => {
const wrapper = mount(Counter)
expect(wrapper.html()).toMatchSnapshot()
})
The first time you run this test, Jest will create a snapshot file. On subsequent runs, it will compare the current output with the stored snapshot.
Advanced Jest Configuration
Mocking API Calls
For components that make API calls, you can use Jest's mocking capabilities:
// Example of mocking axios
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve({ data: { message: 'Hello Jest' } }))
}))
// Then in your test
import axios from 'axios'
it('fetches data when mounted', async () => {
// Your test code here
expect(axios.get).toHaveBeenCalledWith('/api/data')
})
Testing Vuex Store
For testing Vuex stores, you can create a mock store:
import { createStore } from 'vuex' // For Vue 3
// import Vuex from 'vuex' // For Vue 2
// Create a mock store
const store = createStore({ // For Vue 3
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
// In your test
it('commits a mutation', () => {
store.commit('increment')
expect(store.state.count).toBe(1)
})
Testing Vue Router
For components that use Vue Router, you can create a mock router:
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router' // For Vue 3
// import VueRouter from 'vue-router' // For Vue 2
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: { template: '<div>Home</div>' } },
{ path: '/about', component: { template: '<div>About</div>' } }
]
})
// In your test
it('navigates to another route', async () => {
const wrapper = mount(YourComponent, {
global: {
plugins: [router]
}
})
// Test navigation
await router.push('/about')
expect(router.currentRoute.value.path).toBe('/about')
})
Best Practices for Testing Vue.js Components
-
Test outputs, not implementation details: Focus on what the component renders and how it behaves, not on internal details that could change.
-
Use data-testid attributes: Use attributes like
data-testid="count"
to select elements for testing, making your tests more resilient to design changes. -
Isolate components in tests: Mock dependencies like Vuex or API calls to isolate the component being tested.
-
Keep tests simple and focused: Each test should verify one thing. Multiple assertions in a single test can make debugging difficult.
-
Test both happy and error paths: Don't just test that things work when everything goes right; also test error handling.
Troubleshooting Common Issues
SyntaxError: Unexpected token
This often happens when Jest can't process certain file types. Make sure your transformations are properly configured in your Jest config.
Tests pass locally but fail in CI
Check that your CI environment has the same Node.js version and dependencies. Explicit version constraints can help prevent this issue.
Different behavior between test and browser
This is often due to the test environment (jsdom) being different from real browsers. Try using more integration-focused tests for browser-specific behavior.
Slow tests
Consider using jest.mock()
to prevent expensive operations, and run only relevant tests during development with npm run test:watch
.
Real-world Example: Testing a Todo List Component
Let's create a more complex example - a todo list component with tests:
<!-- src/components/TodoList.vue -->
<template>
<div>
<h1>Todo List</h1>
<form @submit.prevent="addTodo">
<input
v-model="newTodo"
data-testid="new-todo-input"
placeholder="Add a todo"
/>
<button type="submit" data-testid="add-todo-button">Add</button>
</form>
<ul>
<li
v-for="(todo, index) in todos"
:key="index"
data-testid="todo-item"
>
<span :class="{ completed: todo.completed }">{{ todo.text }}</span>
<button @click="toggleTodo(index)" data-testid="toggle-button">
{{ todo.completed ? 'Undo' : 'Complete' }}
</button>
<button @click="removeTodo(index)" data-testid="delete-button">
Delete
</button>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
newTodo: '',
todos: []
}
},
methods: {
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
text: this.newTodo,
completed: false
})
this.newTodo = ''
}
},
toggleTodo(index) {
this.todos[index].completed = !this.todos[index].completed
},
removeTodo(index) {
this.todos.splice(index, 1)
}
}
}
</script>
<style scoped>
.completed {
text-decoration: line-through;
color: gray;
}
</style>
Now, let's write tests for this component:
// src/components/__tests__/TodoList.spec.js
import { mount } from '@vue/test-utils'
import TodoList from '../TodoList.vue'
describe('TodoList.vue', () => {
it('renders empty todo list', () => {
const wrapper = mount(TodoList)
expect(wrapper.findAll('[data-testid="todo-item"]')).toHaveLength(0)
})
it('adds a new todo when form is submitted', async () => {
const wrapper = mount(TodoList)
const input = wrapper.find('[data-testid="new-todo-input"]')
const form = wrapper.find('form')
await input.setValue('Test Todo')
await form.trigger('submit')
const todoItems = wrapper.findAll('[data-testid="todo-item"]')
expect(todoItems).toHaveLength(1)
expect(todoItems[0].text()).toContain('Test Todo')
expect(input.element.value).toBe('')
})
it('does not add empty todos', async () => {
const wrapper = mount(TodoList)
const form = wrapper.find('form')
await form.trigger('submit')
expect(wrapper.findAll('[data-testid="todo-item"]')).toHaveLength(0)
})
it('toggles todo completion status', async () => {
const wrapper = mount(TodoList)
const input = wrapper.find('[data-testid="new-todo-input"]')
const form = wrapper.find('form')
await input.setValue('Test Todo')
await form.trigger('submit')
let toggleButton = wrapper.find('[data-testid="toggle-button"]')
await toggleButton.trigger('click')
expect(wrapper.find('.completed').exists()).toBe(true)
expect(toggleButton.text()).toBe('Undo')
await toggleButton.trigger('click')
expect(wrapper.find('.completed').exists()).toBe(false)
expect(toggleButton.text()).toBe('Complete')
})
it('removes a todo', async () => {
const wrapper = mount(TodoList)
const input = wrapper.find('[data-testid="new-todo-input"]')
const form = wrapper.find('form')
await input.setValue('Test Todo')
await form.trigger('submit')
expect(wrapper.findAll('[data-testid="todo-item"]')).toHaveLength(1)
const deleteButton = wrapper.find('[data-testid="delete-button"]')
await deleteButton.trigger('click')
expect(wrapper.findAll('[data-testid="todo-item"]')).toHaveLength(0)
})
})
Summary
In this tutorial, you've learned how to:
- Install and configure Jest for Vue.js applications
- Set up your project structure for testing
- Write basic component tests with Vue Test Utils
- Create snapshots to prevent UI regressions
- Handle more advanced testing scenarios like Vuex and Vue Router
- Apply best practices for Vue.js testing
By setting up Jest correctly and following these patterns, you can build a reliable test suite that gives you confidence when making changes to your Vue.js application.
Additional Resources
Exercises
- Set up Jest in an existing Vue.js project
- Write tests for a form component that validates input
- Create a component that loads data from an API and write tests using mocks
- Add snapshot tests to ensure UI consistency
- Configure CI for your project to run tests automatically on every commit
The more you practice testing, the more natural it will become, and the more robust your applications will be. Happy testing!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)