#Vue.js test
#Why is testing needed?
Testing can ensure code quality, prevent regression errors, and improve code maintainability. There are three main types of tests for Vue applications:
- Unit testing: Test independent functions and components
- Component Testing: Test the behavior and interaction of components
- End-to-End Testing (E2E): Test the complete user flow
#Test tools
#Vitest (recommended)
Vitest is a fast unit testing framework that integrates perfectly with Vite.
npm install -D vitest @vue/test-utils#Configure Vitest
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true
}
})#Unit testing basics
#Test pure functions
// utils/math.js
export function add(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}// utils/math.test.js
import { describe, it, expect } from 'vitest'
import { add, multiply } from './math'
describe('Math Utils', () => {
it('should add two numbers', () => {
expect(add(1, 2)).toBe(3)
expect(add(-1, 1)).toBe(0)
})
it('should multiply two numbers', () => {
expect(multiply(2, 3)).toBe(6)
expect(multiply(0, 5)).toBe(0)
})
})#Test combined functions
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
increment,
decrement,
reset
}
}// composables/useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('should initialize with default value', () => {
const { count } = useCounter()
expect(count.value).toBe(0)
})
it('should initialize with custom value', () => {
const { count } = useCounter(10)
expect(count.value).toBe(10)
})
it('should increment count', () => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
})
it('should decrement count', () => {
const { count, decrement } = useCounter(5)
decrement()
expect(count.value).toBe(4)
})
it('should reset count', () => {
const { count, increment, reset } = useCounter(0)
increment()
increment()
reset()
expect(count.value).toBe(0)
})
})#Component testing
#Test simple components
<!-- components/Counter.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>// components/Counter.test.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter Component', () => {
it('should render initial count', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Count: 0')
})
it('should increment count when button is clicked', 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')
})
})#Test Props
<!-- components/Greeting.vue -->
<script setup>
defineProps({
name: {
type: String,
required: true
},
age: Number
})
</script>
<template>
<div>
<h1>Hello, {{ name }}!</h1>
<p v-if="age">You are {{ age }} years old.</p>
</div>
</template>// components/Greeting.test.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Greeting from './Greeting.vue'
describe('Greeting Component', () => {
it('should render name prop', () => {
const wrapper = mount(Greeting, {
props: {
name: 'Alice'
}
})
expect(wrapper.text()).toContain('Hello, Alice!')
})
it('should render age when provided', () => {
const wrapper = mount(Greeting, {
props: {
name: 'Bob',
age: 25
}
})
expect(wrapper.text()).toContain('You are 25 years old.')
})
it('should not render age when not provided', () => {
const wrapper = mount(Greeting, {
props: {
name: 'Charlie'
}
})
expect(wrapper.text()).not.toContain('years old')
})
})#Test Emits
<!-- components/CustomButton.vue -->
<script setup>
const emit = defineEmits(['click', 'doubleClick'])
function handleClick() {
emit('click', 'Button clicked')
}
function handleDoubleClick() {
emit('doubleClick', 'Button double clicked')
}
</script>
<template>
<button
@click="handleClick"
@dblclick="handleDoubleClick"
>
<slot>Click me</slot>
</button>
</template>// components/CustomButton.test.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import CustomButton from './CustomButton.vue'
describe('CustomButton Component', () => {
it('should emit click event', async () => {
const wrapper = mount(CustomButton)
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
expect(wrapper.emitted('click')).toHaveLength(1)
expect(wrapper.emitted('click')[0]).toEqual(['Button clicked'])
})
it('should emit doubleClick event', async () => {
const wrapper = mount(CustomButton)
await wrapper.trigger('dblclick')
expect(wrapper.emitted()).toHaveProperty('doubleClick')
expect(wrapper.emitted('doubleClick')[0]).toEqual(['Button double clicked'])
})
it('should render slot content', () => {
const wrapper = mount(CustomButton, {
slots: {
default: 'Custom Text'
}
})
expect(wrapper.text()).toBe('Custom Text')
})
})#Test asynchronous operations
<!-- components/UserProfile.vue -->
<script setup>
import { ref, onMounted } from 'vue'
const user = ref(null)
const loading = ref(true)
const error = ref(null)
async function fetchUser() {
try {
const response = await fetch('/api/user')
user.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(() => {
fetchUser()
})
</script>
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</div>
</template>// components/UserProfile.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import UserProfile from './UserProfile.vue'
describe('UserProfile Component', () => {
beforeEach(() => {
global.fetch = vi.fn()
})
it('should show loading state initially', () => {
const wrapper = mount(UserProfile)
expect(wrapper.text()).toContain('Loading...')
})
it('should display user data after loading', async () => {
global.fetch.mockResolvedValue({
json: async () => ({
name: 'John Doe',
email: 'john@example.com'
})
})
const wrapper = mount(UserProfile)
await flushPromises()
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('should display error message on failure', async () => {
global.fetch.mockRejectedValue(new Error('Network error'))
const wrapper = mount(UserProfile)
await flushPromises()
expect(wrapper.text()).toContain('Error: Network error')
})
})#Test Pinia Store
// stores/counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return { count, increment, decrement }
})// stores/counter.test.js
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from './counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with count 0', () => {
const store = useCounterStore()
expect(store.count).toBe(0)
})
it('should increment count', () => {
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1)
})
it('should decrement count', () => {
const store = useCounterStore()
store.count = 5
store.decrement()
expect(store.count).toBe(4)
})
})#Testing Best Practices
#1. Test user behavior, not implementation details
// ❌ 测试实现细节
it('should call increment method', () => {
const wrapper = mount(Counter)
const spy = vi.spyOn(wrapper.vm, 'increment')
wrapper.find('button').trigger('click')
expect(spy).toHaveBeenCalled()
})
// ✅ 测试用户行为
it('should increase count when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})#2. Use data test ID
<template>
<button data-testid="increment-btn" @click="increment">
Increment
</button>
</template>it('should increment when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('[data-testid="increment-btn"]').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})#3. Mock dependencies
// Mock API 调用
vi.mock('./api', () => ({
fetchUser: vi.fn(() => Promise.resolve({ name: 'Test User' }))
}))
// Mock 路由
const mockRouter = {
push: vi.fn()
}
const wrapper = mount(Component, {
global: {
mocks: {
$router: mockRouter
}
}
})#Test coverage
// package.json
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
}
}npm run test:coverage#End-to-end testing (E2E)
Use Playwright or Cypress for E2E testing:
// e2e/login.spec.js (Playwright)
import { test, expect } from '@playwright/test'
test('user can login', async ({ page }) => {
await page.goto('http://localhost:3000')
await page.fill('[data-testid="username"]', 'testuser')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="login-btn"]')
await expect(page.locator('text=Welcome')).toBeVisible()
})#Summarize
- Use Vitest for unit testing and component testing
- Test user behavior, not implementation details
- use
@vue/test-utilsTest Vue components - Mock external dependencies and API calls
- Pursue reasonable test coverage
- Verify the complete process using E2E testing
#Next step
Next, learn Server-Side Rendering (SSR) to learn how to implement server-side rendering.