Files
novalon-manage-system/novalon-manage-web/UNIT_TEST_GUIDE.md
T
张翔 e2ad1331cc feat: 添加测试框架和覆盖率报告功能
feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

fix(测试): 修复测试密码不匹配问题
fix(测试): 修正URL等待策略
fix(测试): 调整错误消息选择器

refactor(测试): 重构测试目录结构
refactor(测试): 优化测试用例组织方式

docs: 更新测试报告文档
docs: 添加测试覆盖率报告模板

ci: 添加Docker测试环境配置
ci: 实现测试自动化脚本

chore: 更新依赖版本
chore: 添加测试相关配置文件
2026-03-25 09:03:37 +08:00

8.8 KiB
Raw Blame History

前端单元测试指南

📋 目录


测试环境配置

Vitest 配置

项目使用 Vitest 作为单元测试框架,配置文件位于 vitest.config.ts

import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*',
        '**/mockData',
        'e2e/',
      ],
      lines: 80,
      functions: 80,
      branches: 80,
      statements: 80,
    },
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

测试脚本

# 运行所有单元测试
npm test

# 运行特定测试文件
npm test -- src/test/auth.test.ts

# 运行测试并生成覆盖率报告
npm run test:coverage

# 运行测试UI界面
npm run test:ui

测试工具函数

createTestHelpers

创建测试辅助函数,简化组件测试:

import { createTestHelpers } from '@/test/utils'
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'

const wrapper = mount(MyComponent)
const helpers = createTestHelpers(wrapper)

// 查找元素
const element = helpers.findByTestId('submit-button')

// 点击元素
await helpers.clickByTestId('submit-button')

// 填写表单
await helpers.fillByTestId('username', 'testuser')

waitFor

等待条件满足:

import { waitFor } from '@/test/utils'

await waitFor(() => {
  return wrapper.text().includes('Success')
})

测试数据

Mock 数据

使用 src/test/fixtures.ts 中的预定义 mock 数据:

import { mockUser, mockRole, mockApiResponse } from '@/test/fixtures'

// 使用 mock 用户数据
const user = mockUser

// 使用 mock API 响应
const response = mockApiResponse(user)

编写单元测试

基础测试示例

1. 测试工具函数

import { describe, it, expect } from 'vitest'
import { formatDate } from '@/utils/date'

describe('formatDate', () => {
  it('should format date correctly', () => {
    const date = new Date('2024-01-01')
    const result = formatDate(date)
    expect(result).toBe('2024-01-01')
  })

  it('should handle null input', () => {
    const result = formatDate(null)
    expect(result).toBe('')
  })
})

2. 测试 Vue 组件

import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import Login from '@/views/system/Login.vue'

describe('Login Component', () => {
  let wrapper: any

  beforeEach(() => {
    wrapper = mount(Login)
  })

  it('should render login form', () => {
    expect(wrapper.find('input[type="text"]').exists()).toBe(true)
    expect(wrapper.find('input[type="password"]').exists()).toBe(true)
    expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
  })

  it('should update username input', async () => {
    const input = wrapper.find('input[type="text"]')
    await input.setValue('testuser')
    expect(input.element.value).toBe('testuser')
  })

  it('should emit login event on form submit', async () => {
    const form = wrapper.find('form')
    await form.trigger('submit.prevent')
    expect(wrapper.emitted('login')).toBeTruthy()
  })
})

3. 测试 API 客户端

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { authApi } from '@/api/auth.api'
import axios from 'axios'

vi.mock('axios')

describe('authApi', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should login successfully', async () => {
    const mockResponse = {
      data: {
        token: 'test-token',
        user: { id: 1, username: 'testuser' }
      }
    }
    vi.mocked(axios.post).mockResolvedValue(mockResponse)

    const result = await authApi.login({ username: 'testuser', password: 'password' })
    
    expect(result.token).toBe('test-token')
    expect(result.user.username).toBe('testuser')
    expect(axios.post).toHaveBeenCalledWith('/auth/login', {
      username: 'testuser',
      password: 'password'
    })
  })
})

4. 测试 Pinia Store

import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('should set user', () => {
    const store = useUserStore()
    const mockUser = { id: 1, username: 'testuser' }
    
    store.setUser(mockUser)
    
    expect(store.user).toEqual(mockUser)
  })

  it('should clear user on logout', () => {
    const store = useUserStore()
    store.setUser({ id: 1, username: 'testuser' })
    
    store.logout()
    
    expect(store.user).toBeNull()
  })
})

测试最佳实践

1. 测试命名

使用清晰的测试名称,描述行为而非实现:

// ✅ 好的测试名称
it('should reject empty username')
it('should display error message when login fails')

// ❌ 不好的测试名称
it('test1')
it('test login function')

2. 测试隔离

每个测试应该独立,不依赖其他测试:

describe('User Component', () => {
  let wrapper: any

  beforeEach(() => {
    // 每个测试前重新创建组件
    wrapper = mount(UserComponent)
  })

  afterEach(() => {
    // 每个测试后清理
    wrapper.unmount()
  })
})

3. 测试覆盖率

确保测试覆盖所有代码路径:

describe('validateEmail', () => {
  it('should accept valid email', () => {
    expect(validateEmail('test@example.com')).toBe(true)
  })

  it('should reject empty email', () => {
    expect(validateEmail('')).toBe(false)
  })

  it('should reject invalid email format', () => {
    expect(validateEmail('invalid')).toBe(false)
  })
})

4. Mock 外部依赖

使用 mock 隔离外部依赖:

import { vi } from 'vitest'

// Mock API 调用
vi.mock('@/api/user.api', () => ({
  userApi: {
    getUsers: vi.fn().mockResolvedValue([])
  }
}))

// Mock 浏览器 API
Object.defineProperty(window, 'localStorage', {
  value: {
    getItem: vi.fn(),
    setItem: vi.fn(),
  }
})

5. 异步测试

正确处理异步操作:

it('should handle async operation', async () => {
  const wrapper = mount(Component)
  
  // 等待异步操作完成
  await wrapper.vm.$nextTick()
  await waitFor(() => wrapper.text().includes('Loaded'))
  
  expect(wrapper.text()).toContain('Loaded')
})

6. 测试用户交互

模拟用户交互:

it('should handle button click', async () => {
  const wrapper = mount(Component)
  const button = wrapper.find('button')
  
  await button.trigger('click')
  await wrapper.vm.$nextTick()
  
  expect(wrapper.emitted('click')).toBeTruthy()
})

it('should handle form submission', async () => {
  const wrapper = mount(Component)
  const form = wrapper.find('form')
  
  await form.trigger('submit.prevent')
  await wrapper.vm.$nextTick()
  
  expect(wrapper.emitted('submit')).toBeTruthy()
})

运行测试

运行所有测试

npm test

运行特定测试文件

npm test -- src/test/auth.test.ts

运行特定测试用例

npm test -- src/test/auth.test.ts -t "should login successfully"

生成覆盖率报告

npm run test:coverage

覆盖率报告将生成在 coverage/ 目录下。


常见问题

1. 测试超时

增加测试超时时间:

it('should handle long operation', async () => {
  // 增加超时时间到 10 秒
}, 10000)

2. Mock 不生效

确保 mock 在导入模块之前:

// ❌ 错误:在导入之后 mock
import { authApi } from '@/api/auth.api'
vi.mock('@/api/auth.api')

// ✅ 正确:在导入之前 mock
vi.mock('@/api/auth.api')
import { authApi } from '@/api/auth.api'

3. 组件渲染问题

使用 shallowMount 替代 mount 减少依赖:

import { shallowMount } from '@vue/test-utils'

const wrapper = shallowMount(Component)

参考资料


最后更新时间: 2026-03-24
维护者: 张翔(全栈质量保障与研发效能工程师)