Testing Vue Applications
Comprehensive testing strategies for Vue applications using Vitest, Vue Test Utils, and end-to-end testing tools:
Vitest - Modern Testing Framework
Vitest is a blazing fast unit test framework powered by Vite. It provides Jest-compatible APIs with better performance.
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
})
// Basic test
import { describe, it, expect } from 'vitest'
import { sum } from './utils'
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3)
})
})Vue Test Utils - Component Testing
Vue Test Utils is the official testing library for Vue components. It provides utilities to mount components and interact with them.
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter.vue', () => {
it('renders initial count', () => {
const wrapper = mount(Counter, {
props: {
initialCount: 5,
},
})
expect(wrapper.text()).toContain('Count: 5')
})
it('increments when button clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Count: 2')
})
it('emits update event', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted()).toHaveProperty('update')
expect(wrapper.emitted('update')).toHaveLength(1)
expect(wrapper.emitted('update')[0]).toEqual([1])
})
})Testing with Pinia Stores
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, it, expect } from 'vitest'
import TodoList from './TodoList.vue'
import { useTodoStore } from '@/stores/todos'
describe('TodoList with Pinia', () => {
beforeEach(() => {
// Create a fresh pinia instance for each test
setActivePinia(createPinia())
})
it('displays todos from store', () => {
const wrapper = mount(TodoList, {
global: {
plugins: [createPinia()],
},
})
const store = useTodoStore()
store.todos = [
{ id: 1, text: 'Test todo', completed: false },
]
expect(wrapper.text()).toContain('Test todo')
})
it('adds todo when form submitted', async () => {
const wrapper = mount(TodoList, {
global: {
plugins: [createPinia()],
},
})
await wrapper.find('input').setValue('New todo')
await wrapper.find('form').trigger('submit')
const store = useTodoStore()
expect(store.todos).toHaveLength(1)
expect(store.todos[0].text).toBe('New todo')
})
})Testing Composables
import { describe, it, expect } from 'vitest'
import { useMouse } from './composables/useMouse'
import { mount } from '@vue/test-utils'
import { defineComponent } from 'vue'
describe('useMouse', () => {
it('tracks mouse position', async () => {
// Create a test component that uses the composable
const TestComponent = defineComponent({
setup() {
const { x, y } = useMouse()
return { x, y }
},
template: '<div>{{ x }}, {{ y }}</div>',
})
const wrapper = mount(TestComponent, {
attachTo: document.body,
})
// Simulate mouse move
const event = new MouseEvent('mousemove', {
clientX: 100,
clientY: 200,
})
window.dispatchEvent(event)
await wrapper.vm.$nextTick()
expect(wrapper.vm.x).toBe(100)
expect(wrapper.vm.y).toBe(200)
wrapper.unmount()
})
})End-to-End Testing with Cypress
// cypress/e2e/todo-app.cy.js
describe('Vue Todo App', () => {
beforeEach(() => {
cy.visit('http://localhost:5173')
})
it('adds and completes todos', () => {
// Add todo
cy.get('[data-test="todo-input"]').type('Buy groceries')
cy.get('[data-test="add-btn"]').click()
// Verify added
cy.contains('Buy groceries').should('be.visible')
// Complete todo
cy.get('[data-test="todo-checkbox"]').first().check()
// Verify completed
cy.get('[data-test="todo-item"]')
.first()
.should('have.class', 'completed')
})
it('filters todos', () => {
// Add multiple todos
const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach(todo => {
cy.get('[data-test="todo-input"]').type(todo)
cy.get('[data-test="add-btn"]').click()
})
// Complete first
cy.get('[data-test="todo-checkbox"]').first().check()
// Filter completed
cy.get('[data-test="filter-completed"]').click()
cy.get('[data-test="todo-item"]').should('have.length', 1)
// Filter active
cy.get('[data-test="filter-active"]').click()
cy.get('[data-test="todo-item"]').should('have.length', 2)
})
})Testing Best Practices
Test behavior, not implementation details
Use data-test attributes for stable selectors
Mock external dependencies (APIs, stores)
Test user interactions with await and $nextTick
Keep tests isolated and independent
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free