Appearance
Vue.js 测试策略
测试类型
Vue.js应用的测试通常包含以下几种类型:
单元测试
单元测试用于测试单个组件或函数的功能。
javascript
// UserCard.test.js
import { mount } from '@vue/test-utils'
import UserCard from '@/components/UserCard.vue'
import { describe, it, expect } from 'vitest'
describe('UserCard', () => {
it('renders user information correctly', () => {
const wrapper = mount(UserCard, {
props: {
user: {
id: 1,
name: 'John Doe',
email: 'john@example.com'
}
}
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('emits event when button is clicked', async () => {
const wrapper = mount(UserCard, {
props: {
user: {
id: 1,
name: 'John Doe',
email: 'john@example.com'
}
}
})
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.emitted('user-action')).toBeTruthy()
})
it('updates class when user is admin', async () => {
const wrapper = mount(UserCard, {
props: {
user: {
id: 1,
name: 'Admin User',
email: 'admin@example.com',
isAdmin: true
}
}
})
expect(wrapper.classes()).toContain('admin-user')
// 测试响应式更新
await wrapper.setProps({
user: { ...wrapper.props().user, isAdmin: false }
})
expect(wrapper.classes()).not.toContain('admin-user')
})
})
集成测试
集成测试用于测试多个组件之间的交互。
javascript
// UserList.test.js
import { mount } from '@vue/test-utils'
import UserList from '@/components/UserList.vue'
import UserCard from '@/components/UserCard.vue'
import { describe, it, expect } from 'vitest'
describe('UserList', () => {
it('renders multiple user cards', () => {
const users = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' }
]
const wrapper = mount(UserList, {
props: {
users: users
}
})
const userCards = wrapper.findAllComponents(UserCard)
expect(userCards).toHaveLength(2)
})
it('emits event when child component emits action', async () => {
const users = [
{ id: 1, name: 'User 1', email: 'user1@example.com' }
]
const wrapper = mount(UserList, {
props: {
users: users
}
})
// 触发子组件事件
const userCard = wrapper.findComponent(UserCard)
await userCard.vm.$emit('user-action', { id: 1 })
expect(wrapper.emitted('user-action')).toBeTruthy()
})
})
端到端测试
端到端测试测试整个应用的工作流程。
javascript
// LoginPage.e2e.js
import { test, expect } from '@playwright/test'
test.describe('Login Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
})
test('allows user to login with valid credentials', async ({ page }) => {
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('.user-profile')).toBeVisible()
})
test('shows error message with invalid credentials', async ({ page }) => {
await page.fill('input[name="email"]', 'invalid@example.com')
await page.fill('input[name="password"]', 'wrongpassword')
await page.click('button[type="submit"]')
await expect(page.locator('.error-message')).toBeVisible()
})
})
测试工具和框架
Vitest + Vue Test Utils
Vitest是Vite生态系统的测试框架,与Vue.js配合使用效果很好。
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.js']
}
})
javascript
// tests/setup.js
import { config } from '@vue/test-utils'
import { vi } from 'vitest'
// 为所有组件全局注册自定义指令
config.global.directives = {
// 自定义指令模拟
'test-directive': {
mounted: (el, binding) => {
el.setAttribute('data-test-mounted', binding.value)
}
}
}
// 模拟全局属性
config.global.mocks = {
$t: (msg) => msg // 模拟国际化
}
组件测试最佳实践
javascript
// FormComponent.test.js
import { mount } from '@vue/test-utils'
import FormComponent from '@/components/FormComponent.vue'
import { describe, it, expect, vi } from 'vitest'
describe('FormComponent', () => {
it('validates form inputs correctly', async () => {
const wrapper = mount(FormComponent)
// 测试邮箱验证
const emailInput = wrapper.find('input[type="email"]')
await emailInput.setValue('invalid-email')
await emailInput.trigger('blur')
expect(wrapper.find('.error').text()).toContain('Invalid email')
// 测试有效邮箱
await emailInput.setValue('valid@example.com')
await emailInput.trigger('blur')
expect(wrapper.find('.error')).not.toExist()
})
it('submits form with valid data', async () => {
const mockSubmit = vi.fn()
const wrapper = mount(FormComponent)
// Mock the submit method
wrapper.vm.onSubmit = mockSubmit
// Fill form
await wrapper.find('input[name="email"]').setValue('test@example.com')
await wrapper.find('input[name="password"]').setValue('password123')
// Submit form
await wrapper.find('form').trigger('submit.prevent')
expect(mockSubmit).toHaveBeenCalled()
})
it('shows loading state during submission', async () => {
const wrapper = mount(FormComponent)
// Mock async submit
wrapper.vm.submitForm = vi.fn().mockImplementation(() => {
return new Promise(resolve => setTimeout(resolve, 100))
})
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.find('.loading').exists()).toBe(true)
// Wait for async operation to complete
await vi.runAllTimers()
expect(wrapper.find('.loading').exists()).toBe(false)
})
})
组合式API测试
测试使用组合式API的组件:
vue
<!-- UserDashboard.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import { fetchUser } from '@/api/user'
const user = ref(null)
const loading = ref(false)
const error = ref(null)
async function loadUser(userId) {
loading.value = true
error.value = null
try {
user.value = await fetchUser(userId)
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
defineProps({
userId: {
type: Number,
required: true
}
})
onMounted(() => {
loadUser(props.userId)
})
</script>
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else-if="user">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
</div>
</template>
javascript
// UserDashboard.test.js
import { mount, flushPromises } from '@vue/test-utils'
import { vi, describe, it, expect } from 'vitest'
import UserDashboard from '@/components/UserDashboard.vue'
// Mock API
vi.mock('@/api/user', () => ({
fetchUser: vi.fn()
}))
import { fetchUser } from '@/api/user'
describe('UserDashboard', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('loads and displays user data', async () => {
const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' }
fetchUser.mockResolvedValue(mockUser)
const wrapper = mount(UserDashboard, {
props: {
userId: 1
}
})
// 等待异步操作完成
await flushPromises()
expect(fetchUser).toHaveBeenCalledWith(1)
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('shows error when API fails', async () => {
const mockError = new Error('Failed to fetch user')
fetchUser.mockRejectedValue(mockError)
const wrapper = mount(UserDashboard, {
props: {
userId: 1
}
})
await flushPromises()
expect(wrapper.text()).toContain('Failed to fetch user')
})
})
测试异步操作
javascript
// AsyncComponent.test.js
import { mount, flushPromises } from '@vue/test-utils'
import AsyncComponent from '@/components/AsyncComponent.vue'
import { describe, it, expect, vi } from 'vitest'
describe('AsyncComponent', () => {
it('handles async data loading', async () => {
const asyncData = { message: 'Hello World' }
// Mock async function
const wrapper = mount(AsyncComponent)
wrapper.vm.loadData = vi.fn().mockResolvedValue(asyncData)
await wrapper.vm.loadData()
expect(wrapper.vm.data).toEqual(asyncData)
expect(wrapper.vm.loading).toBe(false)
})
it('handles async errors gracefully', async () => {
const error = new Error('Async error')
const wrapper = mount(AsyncComponent)
wrapper.vm.loadData = vi.fn().mockRejectedValue(error)
await wrapper.vm.loadData()
expect(wrapper.vm.error).toBe(error.message)
expect(wrapper.vm.loading).toBe(false)
})
it('shows loading state during async operation', async () => {
const asyncData = { message: 'Hello World' }
let resolvePromise
const promise = new Promise(resolve => {
resolvePromise = resolve
})
const wrapper = mount(AsyncComponent)
wrapper.vm.loadData = vi.fn().mockReturnValue(promise)
// Start async operation
const promiseResult = wrapper.vm.loadData()
// Check loading state
expect(wrapper.vm.loading).toBe(true)
// Resolve promise
resolvePromise(asyncData)
await promiseResult
// Check final state
expect(wrapper.vm.data).toEqual(asyncData)
expect(wrapper.vm.loading).toBe(false)
})
})
测试工具函数
javascript
// utils.test.js
import { formatDate, calculateAge, validateEmail } from '@/utils'
import { describe, it, expect } from 'vitest'
describe('Utility Functions', () => {
describe('formatDate', () => {
it('formats date correctly', () => {
const date = new Date('2023-01-15')
expect(formatDate(date)).toBe('2023-01-15')
})
it('handles different date formats', () => {
expect(formatDate('2023-01-15')).toBe('2023-01-15')
expect(formatDate(new Date(2023, 0, 15))).toBe('2023-01-15')
})
})
describe('calculateAge', () => {
it('calculates age correctly', () => {
const birthDate = new Date(Date.now() - 25 * 365 * 24 * 60 * 60 * 1000)
expect(calculateAge(birthDate)).toBe(25)
})
it('handles invalid dates', () => {
expect(calculateAge(null)).toBe(0)
expect(calculateAge('invalid')).toBe(0)
})
})
describe('validateEmail', () => {
it('validates email correctly', () => {
expect(validateEmail('test@example.com')).toBe(true)
expect(validateEmail('invalid-email')).toBe(false)
expect(validateEmail('')).toBe(false)
})
})
})
测试覆盖率
配置测试覆盖率报告:
json
// package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"vitest": "^0.34.0",
"@vitest/coverage-v8": "^0.34.0",
"@vue/test-utils": "^2.4.1"
}
}
通过全面的测试策略,可以确保Vue.js应用的稳定性和可靠性,提高代码质量。