You need to sign in or sign up before continuing.
testing-strategy.md 16.3 KB

微信小程序测试策略指南

文档目的:针对微信小程序的测试挑战,提供完整的测试分层策略和实施方案

项目:Manulife WeApp (Taro 4 + Vue 3)

最后更新:2026-02-22


📊 测试金字塔

本项目采用分层测试策略,各层测试占比:

        /\
       /  \
      /    \         E2E 测试 (5%)
     /      \         - 关键流程验证
    /        \        - H5 + Playwright
   /__________\
  /            \
 /  集成测试    \    集成测试 (10%)
/   (20% 覆盖率)  \   - API 集成
/________________\  - 完整用户流程
/                  \
/     单元测试 (70%)   \ 单元测试 (70%)
/                      - Composables
/                       - Utils
/                        - Components

测试覆盖率目标

层级 目标覆盖率 工具
单元测试 70% Vitest + @vue/test-utils
集成测试 20% Vitest + MSW
E2E 测试 5% Playwright (H5)
手动测试 5% 真机测试

🧪 现有测试基础设施

已配置工具

测试框架

  • Vitest v1.6.0 - 测试运行器
  • @vue/test-utils v2.4.6 - Vue 组件测试
  • happy-dom v14.12.0 - 浏览器环境模拟

测试配置 (vitest.config.js):

export default defineConfig({
  plugins: [ignoreCssPlugin(), vue()],
  test: {
    environment: 'happy-dom',
    css: true,
    globals: true,
    setupFiles: ['./test/setup.ts'
  }
})

现有测试文件

src/
├── composables/__tests__/
│   └── usePlanView.integration.test.js    # 计划视图集成测试
├── utils/__tests__/
│   └── planFieldValidation.test.js      # 字段验证单元测试
├── pages/__tests__/
│   ├── index.test.js                      # 首页测试
│   ├── message-detail.test.js            # 消息详情测试
│   ├── plan.test.js                       # 计划书测试
│   ├── plan-submit-result.test.js       # 提交结果测试
│   └── product-center.test.js           # 产品中心测试
└── __tests__/
    └── tools.test.js                      # 工具函数测试

✅ 单元测试(70%)- 核心重点

测试原则

  • 单一职责:每个测试只验证一个功能点
  • 隔离性:测试之间互不依赖
  • 可重复性:多次运行结果一致
  • 快速执行:单个测试 < 100ms

测试重点

1. Composables 测试

示例:usePermission 测试

// src/composables/__tests__/usePermission.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { usePermission } from '../usePermission'
import * as Taro from '@tarojs/taro'

// Mock Taro API
vi.mock('@tarojs/taro', () => ({
  default: {
    showModal: vi.fn(),
    navigateTo: vi.fn(),
    showToast: vi.fn()
  }
}))

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

  it('should allow access when user is logged in', async () => {
    const { hasPermission } = usePermission()

    // Mock 登录状态
    const { isLoggedIn } = useUserStore()
    isLoggedIn.value = true

    const result = await hasPermission()

    expect(result).toBe(true)
    expect(Taro.showModal).not.toHaveBeenCalled()
  })

  it('should show login modal when user is not logged in', async () => {
    const { hasPermission } = usePermission()

    // Mock 未登录状态
    const { isLoggedIn } = useUserStore()
    isLoggedIn.value = false

    const result = await hasPermission()

    expect(result).toBe(false)
    expect(Taro.showModal).toHaveBeenCalledWith({
      title: '提示',
      content: '请先登录'
    })
  })
})

2. 工具函数测试

示例:字段验证测试

// src/utils/__tests__/planFieldValidation.test.js
import { describe, it, expect } from 'vitest'
import { validateField } from '../planFieldValidation'

describe('planFieldValidation', () => {
  describe('required validation', () => {
    it('should pass when value is provided', () => {
      const result = validateField({ value: 'test' }, { required: true })
      expect(result.valid).toBe(true)
    })

    it('should fail when value is empty', () => {
      const result = validateField({ value: '' }, { required: true })
      expect(result.valid).toBe(false)
      expect(result.error).toContain('必填')
    })
  })

  describe('pattern validation', () => {
    it('should validate email format', () => {
      const result = validateField(
        { value: 'invalid-email' },
        { pattern: 'email' }
      )
      expect(result.valid).toBe(false)
    })

    it('should validate phone format', () => {
      const result = validateField(
        { value: '12345678901' },
        { pattern: 'phone' }
      )
      expect(result.valid).toBe(false)
    })
  })

  describe('range validation', () => {
    it('should validate min value', () => {
      const result = validateField(
        { value: 5 },
        { type: 'number', min: 10 }
      )
      expect(result.valid).toBe(false)
      expect(result.error).toContain('最小值为 10')
    })

    it('should validate max value', () => {
      const result = validateField(
        { value: 200 },
        { type: 'number', max: 100 }
      )
      expect(result.valid).toBe(false)
      expect(result.error).toContain('最大值为 100')
    })
  })
})

3. 组件测试

示例:ProductCard 组件测试

// src/components/__tests__/ProductCard.test.js
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import ProductCard from '../ProductCard.vue'

describe('ProductCard', () => {
  const mockProduct = {
    id: 1,
    title: '测试产品',
    cover: 'https://example.com/cover.jpg',
    tags: ['热销', '推荐']
  }

  it('should render product information correctly', () => {
    const wrapper = mount(ProductCard, {
      props: { product: mockProduct }
    })

    expect(wrapper.text()).toContain('测试产品')
    expect(wrapper.find('img').attributes('src')).toBe('https://example.com/cover.jpg')
  })

  it('should emit click event when clicked', async () => {
    const wrapper = mount(ProductCard, {
      props: { product: mockProduct }
    })

    await wrapper.trigger('click')

    expect(wrapper.emitted('click')).toBeTruthy()
    expect(wrapper.emitted('click')[0]).toEqual([mockProduct])
  })

  it('should show collect button when user is logged in', () => {
    const wrapper = mount(ProductCard, {
      props: {
        product: mockProduct,
        isLoggedIn: true
      }
    })

    expect(wrapper.find('.collect-button').exists()).toBe(true)
  })

  it('should hide collect button when user is not logged in', () => {
    const wrapper = mount(ProductCard, {
      props: {
        product: mockProduct,
        isLoggedIn: false
      }
    })

    expect(wrapper.find('.collect-button').exists()).toBe(false)
  })
})

🔗 集成测试(10%)- 流程验证

测试目标

  • 验证多个模块协作是否正常
  • 测试完整的用户流程
  • 验证 API 集成

测试策略

1. API 集成测试

使用 MSW (Mock Service Worker)

// src/api/__tests__/planAPI.integration.test.js
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { HttpResponse, http } from 'msw'
import { submitPlanAPI, getPlanListAPI } from '../index'

// Setup MSW
const { mock, unmock } = http

beforeEach(() => {
  mock.resetHandlers()
})

afterEach(() => {
  unmock()
})

describe('Plan API Integration', () => {
  const mockPlanData = {
    id: 1,
    name: '测试计划书',
    status: 'pending'
  }

  it('should submit plan successfully', async () => {
    mock.post('/srv/?a=submit_plan', () => {
      return HttpResponse.json({
        code: 1,
        data: mockPlanData,
        msg: '提交成功'
      })
    })

    const result = await submitPlanAPI({
      products: [{ id: 1, amount: 100 }]
    })

    expect(result.code).toBe(1)
    expect(result.data).toEqual(mockPlanData)
  })

  it('should handle API error gracefully', async () => {
    mock.post('/srv/?a=submit_plan', () => {
      return HttpResponse.json({
        code: 0,
        msg: '网络错误'
      }, { status: 500 })
    })

    const result = await submitPlanAPI({
      products: []
    })

    expect(result.code).not.toBe(1)
    expect(result.msg).toBeTruthy()
  })

  it('should retry on 401 error', async () => {
    let requestCount = 0

    mock.post('/srv/?a=submit_plan', () => {
      requestCount++
      if (requestCount === 1) {
        return HttpResponse.json({ code: -1 }, { status: 401 })
      }
      return HttpResponse.json({
        code: 1,
        data: mockPlanData
      })
    })

    const result = await submitPlanAPI({
      products: []
    })

    expect(result.code).toBe(1)
    expect(requestCount).toBe(2) // 第一次 401,第二次成功
  })
})

2. Composable 集成测试

示例:完整用户流程测试

// src/composables/__tests__/usePlanView.integration.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { usePlanView } from '../usePlanView'
import { submitPlanAPI } from '@/api'
import * as Taro from '@tarojs/taro'

vi.mock('@tarojs/taro')
vi.mock('@/api')

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

  it('should complete plan submission flow', async () => {
    const { selectedProducts, submitPlan } = usePlanView()

    // 添加产品
    selectedProducts.value = [
      { id: 1, name: '产品A', amount: 100 },
      { id: 2, name: '产品B', amount: 200 }
    ]

    // Mock API
    submitPlanAPI.mockResolvedValue({
      code: 1,
      data: { planId: 123 }
    })

    // 提交
    const result = await submitPlan()

    expect(result.success).toBe(true)
    expect(result.data.planId).toBe(123)
    expect(submitPlanAPI).toHaveBeenCalledWith({
      products: selectedProducts.value
    })
  })

  it('should show loading state during submission', async () => {
    const { loading, submitPlan } = usePlanView()

    submitPlanAPI.mockImplementation(() => {
      return new Promise(resolve => {
        loading.value = true
        setTimeout(() => {
          loading.value = false
          resolve({ code: 1, data: {} })
        }, 1000)
      })
    })

    const promise = submitPlan()
    expect(loading.value).toBe(true)

    await promise
    expect(loading.value).toBe(false)
  })
})

🎭 E2E 测试(5%)- 关键流程

小程序 E2E 测试方案

由于小程序真机测试困难,我们采用 H5 构建 + Playwright 的替代方案:

方案对比

方案 优势 劣势 推荐指数
微信开发者工具自动化 原生支持 功能有限,不稳定 ⭐⭐
H5 + Playwright 成熟稳定,调试方便 非原生环境 ⭐⭐⭐⭐⭐
真机云测试 真实环境 成本高,配置复杂 ⭐⭐⭐

H5 构建 + Playwright 实施

1. 创建 E2E 测试文件

// e2e/plan-workflow.spec.js
import { test, expect } from '@playwright/test'

test.describe('计划书提交流程', () => {
  test.beforeEach(async ({ page }) => {
    // 导航到首页
    await page.goto('/#/')

    // 登录(如果需要)
    const loginButton = page.locator('text=登录')
    if (await loginButton.isVisible()) {
      await loginButton.click()
      // 填写登录信息...
    }
  })

  test('should complete plan submission', async ({ page }) => {
    // 1. 进入产品中心
    await page.click('text=产品中心')
    await expect(page).toHaveURL(/.*product-center/)

    // 2. 选择产品
    await page.click('[data-product-id="1"]')
    await page.click('text=加入计划书')

    // 3. 进入计划书页面
    await page.click('text=计划书')
    await expect(page).toHaveURL(/.*plan/)

    // 4. 提交计划书
    await page.click('text=提交计划书')

    // 验证成功提示
    await expect(page.locator('text=提交成功')).toBeVisible()
  })

  test('should show error when product list is empty', async ({ page }) => {
    await page.goto('/#/plan')

    // 空列表应该显示提示
    await expect(page.locator('text=请先添加产品')).toBeVisible()
  })
})

2. Playwright 配置

// playwright.config.js
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  fullyParallel: false,
  forbidOnly: false,
  retries: 1,

  use: {
    baseURL: 'http://localhost:5173/#/',
    headless: false,
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure'
  },

  projects: [
    {
      name: 'chromium',
      use: {
        viewport: { width: 375, height: 667 }, // 小程序尺寸
        locale: 'zh-CN'
      }
    }
  ]
})

3. 运行 E2E 测试

# 1. 构建 H5 版本
pnpm dev:h5

# 2. 在另一个终端运行 E2E 测试
pnpm test:e2e

# 3. 查看测试报告
# 自动生成在 playwright-report/

📋 实施路线图

短期目标(1-2 周)✅

目标:完善单元测试覆盖率

  • 为所有 Composables 添加测试
  • 为工具函数添加测试
  • 为核心组件添加测试
  • 达到 70% 单元测试覆盖率

命令

# 运行所有测试
pnpm test

# 查看覆盖率报告
pnpm test:coverage

中期目标(2-4 周)

目标:添加集成测试

  • 为关键 API 流程添加集成测试
  • 为完整用户流程添加集成测试
  • 配置 MSW 用于 API Mock
  • 达到 80% 测试覆盖率

新增依赖

pnpm add -D msw

长期目标(1-2 月)

目标:完善 E2E 测试

  • 配置 Playwright
  • 为关键流程编写 E2E 测试
  • 配置 CI/CD 自动测试
  • 达到 90% 测试覆盖率

新增依赖

pnpm add -D @playwright/test

✅ 测试最佳实践

1. Mock 策略

Mock Taro API

vi.mock('@tarojs/taro', () => ({
  default: {
    navigateTo: vi.fn(),
    showModal: vi.fn(),
    showToast: vi.fn(),
    getSystemInfo: vi.fn(() => Promise.resolve({
      model: 'iPhone 12'
    }))
  }
}))

Mock Pinia Store

import { setActivePinia, createPinia } from 'pinia'
import { createTestingApp } from '@pinia/testing'

const testingApp = createTestingApp({
  state: {
    user: {
      isLoggedIn: true,
      userInfo: { name: 'Test User' }
    }
  }
})

2. 测试命名规范

使用描述性命名

// ✅ GOOD
it('should show error message when form is invalid')

// ❌ BAD
it('test 1')

3. AAA 模式(Arrange-Act-Assert)

it('should add product to cart', () => {
  // Arrange - 准备测试数据
  const product = { id: 1, name: '产品A' }
  const cart = []

  // Act - 执行操作
  const result = addToCart(product, cart)

  // Assert - 验证结果
  expect(result).toContain(product)
  expect(cart.length).toBe(1)
})

4. 边界测试

测试边界值和异常情况

describe('boundary testing', () => {
  it('should handle empty input')
  it('should handle null input')
  it('should handle max value')
  it('should handle min value')
  it('should handle network timeout')
})

🔍 测试检查清单

提交代码前

  • 所有新功能有对应的单元测试
  • 测试覆盖率不降低
  • 所有测试通过 (pnpm test)
  • 无跳过的测试 (test.skip())

发布前

  • E2E 测试通过
  • 关键流程手动测试通过
  • 测试覆盖率 ≥ 80%
  • 性能测试通过

🛠️ 常用测试命令

# 运行所有测试
pnpm test

# 运行特定测试文件
pnpm test usePermission.test.js

# 运行匹配的测试
pnpm test --validation

# 监听模式(开发时使用)
pnpm test --watch

# 覆盖率报告
pnpm test:coverage

# UI 模式(交互式调试)
pnpm test --ui

📚 相关文档


维护说明

  • 本文档随测试基础设施更新而维护
  • 有新的测试模式或工具时,及时更新
  • 定期审查测试覆盖率,确保测试质量

最后更新: 2026-02-22