Skip to content
On this page

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应用的稳定性和可靠性,提高代码质量。