Skip to main content

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:

bash
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:

bash
npm install --save-dev jest @vue/test-utils vue-jest @vue/vue3-jest babel-jest

For Vue 2.x projects, use this configuration instead:

bash
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:

javascript
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:

javascript
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 environment
  • transform: Defines how different file types should be processed
  • moduleFileExtensions: File types Jest should look for and process
  • moduleNameMapper: Maps import paths (like @/components) to directory locations
  • testMatch: Patterns to match test files
  • collectCoverage: Enables test coverage reporting
  • collectCoverageFrom: 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:

javascript
module.exports = {
presets: [
'@babel/preset-env'
]
};

Adding Test Scripts

Add Jest test scripts to your package.json file:

json
{
"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:

html
<!-- 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:

javascript
// 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

  1. We import mount from Vue Test Utils to create a wrapper around our component
  2. We create test cases using Jest's describe and it functions
  3. We interact with the component using Vue Test Utils methods:
    • find() to select elements
    • text() to get text content
    • trigger() to simulate events

Running Tests

Now run your tests using the command:

bash
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:

javascript
// 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:

javascript
// 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:

javascript
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:

javascript
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

  1. Test outputs, not implementation details: Focus on what the component renders and how it behaves, not on internal details that could change.

  2. Use data-testid attributes: Use attributes like data-testid="count" to select elements for testing, making your tests more resilient to design changes.

  3. Isolate components in tests: Mock dependencies like Vuex or API calls to isolate the component being tested.

  4. Keep tests simple and focused: Each test should verify one thing. Multiple assertions in a single test can make debugging difficult.

  5. 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:

html
<!-- 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:

javascript
// 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:

  1. Install and configure Jest for Vue.js applications
  2. Set up your project structure for testing
  3. Write basic component tests with Vue Test Utils
  4. Create snapshots to prevent UI regressions
  5. Handle more advanced testing scenarios like Vuex and Vue Router
  6. 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

  1. Set up Jest in an existing Vue.js project
  2. Write tests for a form component that validates input
  3. Create a component that loads data from an API and write tests using mocks
  4. Add snapshot tests to ensure UI consistency
  5. 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! :)