Files
admin-vben/docs/测试文档.md

13 KiB

测试文档

测试概述

AIOTAGRO 管理系统采用全面的测试策略,确保系统质量和稳定性。测试覆盖单元测试、集成测试和端到端测试。

测试环境

环境要求

  • Node.js: 18.0.0+
  • pnpm: 8.0.0+
  • 浏览器: Chrome 90+, Firefox 85+, Safari 14+

测试工具栈

工具 版本 用途
Vitest 1.0.0+ 单元测试框架
Vue Test Utils 2.4.0+ Vue 组件测试
Playwright 1.40.0+ E2E 测试
Testing Library 6.0.0+ 组件测试工具

单元测试

测试配置

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    include: ['**/__tests__/**/*.spec.ts'],
    coverage: {
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'dist/',
        '**/*.d.ts',
        '**/types/**'
      ]
    }
  }
})

组件测试示例

// __tests__/components/UserInfo.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import UserInfo from '@/components/UserInfo.vue'

describe('UserInfo', () => {
  it('renders user information correctly', () => {
    const user = {
      id: 1,
      name: '张三',
      email: 'zhangsan@example.com',
      avatar: '/avatar.jpg'
    }

    const wrapper = mount(UserInfo, {
      props: { user }
    })

    expect(wrapper.text()).toContain('张三')
    expect(wrapper.text()).toContain('zhangsan@example.com')
    expect(wrapper.find('img').attributes('src')).toBe('/avatar.jpg')
  })

  it('emits edit event when edit button is clicked', async () => {
    const user = {
      id: 1,
      name: '张三',
      email: 'zhangsan@example.com'
    }

    const wrapper = mount(UserInfo, {
      props: { user }
    })

    await wrapper.find('[data-testid="edit-btn"]').trigger('click')
    expect(wrapper.emitted('edit')).toBeTruthy()
  })
})

工具函数测试

// __tests__/utils/format.spec.ts
import { describe, it, expect } from 'vitest'
import { formatDate, formatCurrency } from '@/utils/format'

describe('format utils', () => {
  describe('formatDate', () => {
    it('formats date correctly', () => {
      const date = new Date('2023-12-01')
      expect(formatDate(date)).toBe('2023-12-01')
    })

    it('handles invalid date', () => {
      expect(formatDate(null)).toBe('')
      expect(formatDate(undefined)).toBe('')
    })
  })

  describe('formatCurrency', () => {
    it('formats currency correctly', () => {
      expect(formatCurrency(1234.56)).toBe('¥1,234.56')
      expect(formatCurrency(0)).toBe('¥0.00')
    })
  })
})

集成测试

API 集成测试

// __tests__/api/user.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { userApi } from '@/api/user'
import { mockServer } from '../mocks/server'

describe('User API', () => {
  beforeEach(() => {
    mockServer.listen()
  })

  afterEach(() => {
    mockServer.resetHandlers()
  })

  afterAll(() => {
    mockServer.close()
  })

  it('fetches user list successfully', async () => {
    const response = await userApi.getUsers({ page: 1, size: 10 })
    
    expect(response.status).toBe(200)
    expect(response.data.list).toHaveLength(2)
    expect(response.data.total).toBe(2)
  })

  it('handles API errors correctly', async () => {
    mockServer.use(
      rest.get('/api/users', (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ message: 'Internal Server Error' }))
      })
    )

    await expect(userApi.getUsers({ page: 1, size: 10 })).rejects.toThrow()
  })
})

状态管理测试

// __tests__/stores/user.spec.ts
import { describe, it, expect } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'

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

  it('initializes with default values', () => {
    const store = useUserStore()
    
    expect(store.user).toBeNull()
    expect(store.isLoggedIn).toBe(false)
    expect(store.loading).toBe(false)
  })

  it('sets user correctly', () => {
    const store = useUserStore()
    const user = { id: 1, name: '张三', email: 'zhangsan@example.com' }
    
    store.setUser(user)
    
    expect(store.user).toEqual(user)
    expect(store.isLoggedIn).toBe(true)
  })

  it('clears user on logout', () => {
    const store = useUserStore()
    const user = { id: 1, name: '张三', email: 'zhangsan@example.com' }
    
    store.setUser(user)
    store.logout()
    
    expect(store.user).toBeNull()
    expect(store.isLoggedIn).toBe(false)
  })
})

E2E 测试

测试配置

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './__tests__/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],

  webServer: {
    command: 'pnpm dev:antd',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

登录流程测试

// __tests__/e2e/login.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Login Flow', () => {
  test('should login successfully with valid credentials', async ({ page }) => {
    await page.goto('/login')
    
    // 填写登录表单
    await page.fill('[data-testid="username"]', 'admin')
    await page.fill('[data-testid="password"]', '123456')
    await page.click('[data-testid="login-btn"]')
    
    // 验证登录成功
    await expect(page).toHaveURL('/dashboard')
    await expect(page.locator('[data-testid="user-name"]')).toContainText('管理员')
  })

  test('should show error message with invalid credentials', async ({ page }) => {
    await page.goto('/login')
    
    // 填写错误凭证
    await page.fill('[data-testid="username"]', 'wronguser')
    await page.fill('[data-testid="password"]', 'wrongpass')
    await page.click('[data-testid="login-btn"]')
    
    // 验证错误提示
    await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
    await expect(page).toHaveURL('/login')
  })

  test('should redirect to login when accessing protected page without authentication', async ({ page }) => {
    // 直接访问受保护页面
    await page.goto('/user-management')
    
    // 验证重定向到登录页
    await expect(page).toHaveURL('/login')
  })
})

用户管理测试

// __tests__/e2e/user-management.spec.ts
import { test, expect } from '@playwright/test'

test.describe('User Management', () => {
  test.beforeEach(async ({ page }) => {
    // 登录
    await page.goto('/login')
    await page.fill('[data-testid="username"]', 'admin')
    await page.fill('[data-testid="password"]', '123456')
    await page.click('[data-testid="login-btn"]')
    await page.goto('/user-management')
  })

  test('should display user list', async ({ page }) => {
    await expect(page.locator('[data-testid="user-table"]')).toBeVisible()
    await expect(page.locator('[data-testid="user-row"]').first()).toBeVisible()
  })

  test('should create new user', async ({ page }) => {
    // 点击新建按钮
    await page.click('[data-testid="create-user-btn"]')
    
    // 填写用户信息
    await page.fill('[data-testid="user-name"]', '测试用户')
    await page.fill('[data-testid="user-email"]', 'test@example.com')
    await page.click('[data-testid="save-btn"]')
    
    // 验证创建成功
    await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
    await expect(page.locator('[data-testid="user-table"]')).toContainText('测试用户')
  })

  test('should delete user', async ({ page }) => {
    // 点击删除按钮
    await page.click('[data-testid="delete-user-btn"]').first()
    await page.click('[data-testid="confirm-delete-btn"]')
    
    // 验证删除成功
    await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
  })
})

性能测试

加载性能测试

// __tests__/performance/loading.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Performance Tests', () => {
  test('should load dashboard within 3 seconds', async ({ page }) => {
    const startTime = Date.now()
    await page.goto('/dashboard')
    const loadTime = Date.now() - startTime
    
    expect(loadTime).toBeLessThan(3000)
  })

  test('should render user table with 1000 rows efficiently', async ({ page }) => {
    await page.goto('/user-management')
    
    // 模拟大量数据
    await page.evaluate(() => {
      window.performance.mark('table-render-start')
    })
    
    // 等待表格渲染完成
    await page.waitForSelector('[data-testid="user-table"]')
    
    const renderTime = await page.evaluate(() => {
      window.performance.mark('table-render-end')
      window.performance.measure('table-render', 'table-render-start', 'table-render-end')
      const measure = window.performance.getEntriesByName('table-render')[0]
      return measure.duration
    })
    
    expect(renderTime).toBeLessThan(1000)
  })
})

安全测试

XSS 防护测试

// __tests__/security/xss.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Security Tests', () => {
  test('should sanitize user input to prevent XSS', async ({ page }) => {
    await page.goto('/user-management')
    
    // 尝试注入 XSS 代码
    const xssPayload = '<script>alert("XSS")</script>'
    await page.fill('[data-testid="user-name"]', xssPayload)
    await page.click('[data-testid="save-btn"]')
    
    // 验证输入被正确转义
    const userNameCell = await page.locator('[data-testid="user-name"]').first()
    const innerHTML = await userNameCell.innerHTML()
    
    expect(innerHTML).not.toContain('<script>')
    expect(innerHTML).toContain('<script>')
  })
})

测试覆盖率

覆盖率配置

{
  "coverage": {
    "provider": "v8",
    "reporter": ["text", "json", "html"],
    "reportsDirectory": "./coverage",
    "exclude": [
      "**/*.d.ts",
      "**/types/**",
      "**/node_modules/**",
      "**/dist/**",
      "**/coverage/**"
    ],
    "thresholds": {
      "lines": 80,
      "functions": 80,
      "branches": 70,
      "statements": 80
    }
  }
}

覆盖率报告

# 生成覆盖率报告
pnpm test:coverage

# 查看 HTML 报告
open coverage/index.html

测试执行

开发环境测试

# 运行单元测试
pnpm test:unit

# 运行 E2E 测试
pnpm test:e2e

# 运行所有测试
pnpm test

CI/CD 环境测试

# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      
      - run: pnpm install
      - run: pnpm test:unit
      - run: pnpm test:e2e
      - run: pnpm test:coverage

测试最佳实践

1. 测试命名规范

  • 描述性测试名称
  • 使用 Given-When-Then 模式
  • 避免模糊的测试描述

2. 测试数据管理

  • 使用测试工厂函数
  • 避免硬编码数据
  • 清理测试数据

3. 测试隔离

  • 每个测试独立运行
  • 避免测试间依赖
  • 使用 beforeEach/afterEach

4. 异步测试

  • 正确处理异步操作
  • 使用适当的等待策略
  • 避免不必要的等待

故障排除

常见问题

1. 测试超时

// 增加超时时间
test('slow operation', async ({ page }) => {
  test.setTimeout(60000)
  // 测试代码
})

2. 元素找不到

// 使用更稳定的选择器
await page.locator('[data-testid="submit-btn"]').click()

3. 网络请求失败

// 等待网络请求完成
await page.waitForResponse(response => 
  response.url().includes('/api/users') && response.status() === 200
)

测试报告

生成测试报告

# 生成 JUnit 报告
pnpm test:report

# 生成覆盖率报告
pnpm test:coverage:report

报告解读

  • 测试通过率: 应保持在 95% 以上
  • 代码覆盖率: 单元测试覆盖率应达到 80% 以上
  • 性能指标: 关键页面加载时间应小于 3 秒
  • 安全测试: 所有安全测试必须通过