Vue Testing (Vitest/Jest)
Write reliable tests for your Vue components
๐งช Testing Vue Apps
Testing ensures your Vue components work correctly and prevents bugs. Use Vitest for modern Vite projects or Jest for traditional setups with Vue Test Utils for component testing.
// Basic component test
import { mount } from '@vue/test-utils'
import MyButton from './MyButton.vue'
test('button displays text', () => {
const wrapper = mount(MyButton, {
props: { text: 'Click me' }
})
expect(wrapper.text()).toBe('Click me')
})
Testing Essentials
Vitest
Fast Vite-native testing
import { test, expect } from 'vitest'
test('adds numbers', () => {
expect(1 + 1).toBe(2)
})
Test Utils
Mount and test components
const wrapper = mount(Component)
wrapper.find('button').trigger('click')
User Events
Simulate user interactions
await wrapper.find('input')
.setValue('test')
Snapshots
Track component output
expect(wrapper.html())
.toMatchSnapshot()
๐น Setting Up Vitest
Configure Vitest for your Vue project:
# Install dependencies
npm install -D vitest @vue/test-utils jsdom
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom'
}
})
// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest --coverage"
}
}
๐น Basic Component Testing
Test component rendering and props:
<!-- Counter.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
// Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('renders initial count', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Count: 0')
})
it('increments count on button click', async () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.text()).toContain('Count: 1')
await button.trigger('click')
expect(wrapper.text()).toContain('Count: 2')
})
})
๐น Testing Props and Events
Verify component props and emitted events:
<!-- UserCard.vue -->
<script setup>
defineProps(['name', 'email'])
const emit = defineEmits(['delete'])
</script>
<template>
<div class="user-card">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
<button @click="emit('delete')">Delete</button>
</div>
</template>
// UserCard.test.js
import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'
describe('UserCard', () => {
it('displays user information', () => {
const wrapper = mount(UserCard, {
props: {
name: 'John Doe',
email: '[email protected]'
}
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('[email protected]')
})
it('emits delete event when button clicked', async () => {
const wrapper = mount(UserCard, {
props: { name: 'John', email: '[email protected]' }
})
await wrapper.find('button').trigger('click')
expect(wrapper.emitted()).toHaveProperty('delete')
expect(wrapper.emitted('delete')).toHaveLength(1)
})
})
๐น Testing Composables
Test Vue composables and custom hooks:
// useCounter.js
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initial
return { count, increment, decrement, reset }
}
// useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('initializes with default value', () => {
const { count } = useCounter()
expect(count.value).toBe(0)
})
it('initializes with custom value', () => {
const { count } = useCounter(10)
expect(count.value).toBe(10)
})
it('increments count', () => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
})
it('resets to initial value', () => {
const { count, increment, reset } = useCounter(5)
increment()
increment()
reset()
expect(count.value).toBe(5)
})
})
๐น Mocking and Stubs
Mock dependencies and child components:
// Component with API call
import { mount } from '@vue/test-utils'
import { vi } from 'vitest'
import UserList from './UserList.vue'
describe('UserList', () => {
it('fetches and displays users', async () => {
// Mock API call
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
])
})
)
const wrapper = mount(UserList)
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('John')
expect(wrapper.text()).toContain('Jane')
})
it('stubs child component', () => {
const wrapper = mount(ParentComponent, {
global: {
stubs: {
ChildComponent: true // Stub child
}
}
})
expect(wrapper.findComponent({ name: 'ChildComponent' }).exists()).toBe(true)
})
})
๐น Testing with Pinia
Test components that use Pinia stores:
// Component.test.js
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/todo'
describe('TodoList', () => {
beforeEach(() => {
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')
})
})
๐น Running Tests
Execute and monitor your test suite:
# Run all tests
npm run test
# Watch mode (re-run on changes)
npm run test -- --watch
# Run specific test file
npm run test Counter.test.js
# Run with coverage report
npm run coverage
# UI mode (visual test runner)
npm run test:ui
Test Best Practices:
- Test behavior, not implementation
- Keep tests simple and focused
- Use descriptive test names
- Mock external dependencies
- Aim for good coverage, not 100%