testing-strategy.md 24 KB

测试策略

版本信息

版本 日期 描述 作者
2.9 2025-12-15 添加API模拟规范和前端组件测试策略 James
2.8 2025-11-11 更新包测试结构,添加模块化包测试策略 Winston
2.7 2025-11-09 更新为monorepo测试架构,清理重复测试文件 James
2.6 2025-10-15 完成遗留测试文件迁移到统一的tests目录结构 Winston
2.5 2025-10-14 更新测试文件位置到统一的tests目录结构 Claude
2.4 2025-09-20 更新测试策略与主架构文档版本一致 Winston

概述

本文档定义了D8D Starter项目的完整测试策略,基于monorepo架构和现有的测试基础设施。测试策略遵循测试金字塔模型,确保代码质量、功能稳定性和系统可靠性。

测试架构更新 (v2.8)

项目已重构为模块化包架构,测试架构相应调整为:

  • 基础设施包: shared-types、shared-utils、shared-crud、shared-test-util
  • 业务模块包: user-module、auth-module、file-module、geo-areas
  • 应用层: server (重构后),包含模块集成测试
  • web: Web应用,包含组件测试、集成测试和E2E测试
  • CI/CD: 独立的工作流分别处理各包的测试

包测试架构 (v2.8)

项目采用分层测试架构,每个包独立测试:

  • 基础设施包: 纯单元测试,不依赖外部服务
  • 业务模块包: 单元测试 + 集成测试,验证模块功能
  • 应用层: 集成测试,验证模块间协作
  • 共享测试工具: shared-test-util 提供统一的测试基础设施

测试金字塔策略

单元测试 (Unit Tests)

  • 范围: 单个函数、类或组件
  • 目标: 验证独立单元的correctness
  • 位置:
    • 基础设施包: packages/shared-*/tests/unit/**/*.test.ts
    • 业务模块包: packages/*-module/tests/unit/**/*.test.ts
    • server包: packages/server/tests/unit/**/*.test.ts
    • web应用: web/tests/unit/**/*.test.{ts,tsx}
  • 框架: Vitest
  • 覆盖率目标: ≥ 80%
  • 执行频率: 每次代码变更

集成测试 (Integration Tests)

  • 范围: 多个组件/服务协作
  • 目标: 验证模块间集成和交互
  • 位置:
    • 业务模块包: packages/*-module/tests/integration/**/*.test.ts
    • server包: packages/server/tests/integration/**/*.test.ts (模块集成测试)
    • web应用: web/tests/integration/**/*.test.{ts,tsx}
  • 框架: Vitest + Testing Library + hono/testing + shared-test-util
  • 覆盖率目标: ≥ 60%
  • 执行频率: 每次API变更

E2E测试 (End-to-End Tests)

  • 范围: 完整用户流程
  • 目标: 验证端到端业务流程
  • 位置: web/tests/e2e/**/*.test.{ts,tsx}
  • 框架: Playwright
  • 覆盖率目标: 关键用户流程100%
  • 执行频率: 每日或每次重大变更

测试环境配置

开发环境

// vitest.config.ts - 开发环境配置
export default defineConfig({
  test: {
    projects: [
      // Node.js 环境项目 - 后端测试
      {
        test: {
          include: [
            'tests/unit/server/**/*.test.{ts,js}',
            'tests/integration/server/**/*.test.{ts,js}'
          ],
          // ... 其他配置
        }
      },
      // Happy DOM 环境项目 - 前端组件测试
      {
        test: {
          include: [
            'tests/unit/client/**/*.test.{ts,js,tsx,jsx}',
            'tests/integration/client/**/*.test.{ts,js,tsx,jsx}'
          ],
          // ... 其他配置
        }
      }
    ]
  }
});

CI/CD环境

# GitHub Actions 测试配置 (模块化包架构)
name: Test Pipeline

jobs:
  # 基础设施包测试
  shared-packages-tests:
    runs-on: ubuntu-latest
    steps:
      - run: cd packages/shared-types && pnpm test
      - run: cd packages/shared-utils && pnpm test
      - run: cd packages/shared-crud && pnpm test
      - run: cd packages/shared-test-util && pnpm test

  # 业务模块包测试
  business-modules-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: test_password
          POSTGRES_DB: test_d8dai
    steps:
      - run: cd packages/user-module && pnpm test
      - run: cd packages/auth-module && pnpm test
      - run: cd packages/file-module && pnpm test
      - run: cd packages/geo-areas && pnpm test

  # 服务器集成测试
  server-integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: test_password
          POSTGRES_DB: test_d8dai
    steps:
      - run: cd packages/server && pnpm test

  # Web应用测试
  web-integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: test_password
          POSTGRES_DB: test_d8dai
    steps:
      - run: cd web && pnpm test:integration

  web-component-tests:
    runs-on: ubuntu-latest
    steps:
      - run: cd web && pnpm test:components

  web-e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - run: cd web && pnpm test:e2e:chromium

测试覆盖率标准

各层覆盖率要求

测试类型 最低要求 目标要求 关键模块要求
单元测试 70% 80% 90%
集成测试 50% 60% 70%
E2E测试 关键流程100% 主要流程80% -

关键模块定义

  • 认证授权模块: 必须达到90%单元测试覆盖率
  • 数据库操作模块: 必须达到85%单元测试覆盖率
  • 核心业务逻辑: 必须达到80%集成测试覆盖率
  • 用户管理功能: 必须100% E2E测试覆盖

测试数据管理

测试数据策略

// 测试数据工厂模式
export function createTestUser(overrides = {}): User {
  return {
    id: 1,
    username: 'testuser',
    email: 'test@example.com',
    createdAt: new Date(),
    ...overrides
  };
}

// 使用示例
const adminUser = createTestUser({ role: 'admin' });
const inactiveUser = createTestUser({ active: false });

数据库测试策略

  • 单元测试: 使用内存数据库或完全mock
  • 集成测试: 使用专用测试数据库,事务回滚
  • E2E测试: 使用接近生产环境的数据库

数据清理策略

  1. 事务回滚 (推荐)
  2. 数据库清理 (每个测试后)
  3. 测试数据隔离 (使用唯一标识符)

API模拟规范

概述

API模拟规范为管理后台UI包提供测试中的API模拟策略。虽然当前项目实践中每个UI包都有自己的客户端管理器,但为了简化测试复杂度、特别是跨UI包集成测试场景,规范要求统一模拟共享UI组件包中的rpcClient函数。

问题背景

当前实现中,每个UI包测试文件模拟自己的客户端管理器(如AdvertisementClientManagerUserClientManager)。这种模式在单一UI包测试时可行,但在跨UI包集成测试时存在严重问题:

示例场景:收货地址UI包中使用区域管理UI包的区域选择组件,两个组件分别使用各自的客户端管理器。

  • 收货地址组件 → 使用DeliveryAddressClientManager
  • 区域选择组件 → 使用AreaClientManager
  • 测试时需要同时模拟两个客户端管理器,配置复杂且容易冲突

统一模拟优势:通过模拟@d8d/shared-ui-components包中的rpcClient函数,可以:

  1. 统一控制:所有API调用都经过同一个模拟点
  2. 简化配置:无需关心具体客户端管理器,只需配置API响应
  3. 跨包支持:天然支持多个UI包组件的集成测试
  4. 维护性:API响应配置集中管理,易于更新

现有模式分析(仅供参考)

项目中的管理后台UI包当前遵循以下架构模式:

  1. 客户端管理器模式:每个UI包都有一个客户端管理器类(如AdvertisementClientManagerUserClientManager
  2. rpcClient使用:客户端管理器使用@d8d/shared-ui-components包中的rpcClient函数创建Hono RPC客户端
  3. API结构:生成的客户端使用Hono风格的方法调用(如index.$getindex.$post:id.$put等)

注意:新的测试规范要求直接模拟rpcClient函数,而不是模拟各个客户端管理器。

rpcClient函数分析

rpcClient函数位于@d8d/shared-ui-components包的src/utils/hc.ts文件中,其核心功能是创建Hono RPC客户端:

// packages/shared-ui-components/src/utils/hc.ts
export const rpcClient = <T extends Hono<any, any, any>>(aptBaseUrl: string): ReturnType<typeof hc<T>> => {
  return hc<T>(aptBaseUrl, {
    fetch: axiosFetch
  })
}

该函数接收API基础URL参数,返回一个配置了axios适配器的Hono客户端实例。

模拟策略

1. 统一模拟rpcClient函数

在测试中,使用Vitest的vi.mock直接模拟@d8d/shared-ui-components包中的rpcClient函数,统一拦截所有API调用:

// 测试文件顶部 - 统一模拟rpcClient函数
import { vi } from 'vitest'
import type { Hono } from 'hono'

// 创建模拟的rpcClient函数
const mockRpcClient = vi.fn((aptBaseUrl: string) => {
  // 创建模拟的Hono客户端结构
  const mockClient = {
    // 支持动态路径访问
    [Symbol.toPrimitive]: () => mockClient,

    // 通用API端点模拟
    index: {
      $get: vi.fn(),
      $post: vi.fn(),
      $put: vi.fn(),
      $delete: vi.fn(),
    },
    ':id': {
      $get: vi.fn(),
      $put: vi.fn(),
      $delete: vi.fn(),
    },

    // 支持嵌套路径访问
    $path: (path: string) => {
      // 根据路径返回对应的模拟端点
      const pathSegments = path.split('/').filter(Boolean)
      let current = mockClient
      for (const segment of pathSegments) {
        if (!current[segment]) {
          current[segment] = {
            $get: vi.fn(),
            $post: vi.fn(),
            $put: vi.fn(),
            $delete: vi.fn(),
          }
        }
        current = current[segment]
      }
      return current
    }
  }

  return mockClient
})

// 模拟共享UI组件包中的rpcClient函数
vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
  rpcClient: mockRpcClient
}))

2. 创建模拟响应辅助函数

创建通用的模拟响应辅助函数,用于生成一致的API响应格式:

// 在测试文件中定义或从共享工具导入
const createMockResponse = (status: number, data?: any) => ({
  status,
  ok: status >= 200 && status < 300,
  body: null,
  bodyUsed: false,
  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
  headers: new Headers(),
  url: '',
  redirected: false,
  type: 'basic' as ResponseType,
  json: async () => data || {},
  text: async () => '',
  blob: async () => new Blob(),
  arrayBuffer: async () => new ArrayBuffer(0),
  formData: async () => new FormData(),
  clone: function() { return this; }
});

// 创建简化版响应工厂(针对常见业务数据结构)
const createMockApiResponse = <T>(data: T, success = true) => ({
  success,
  data,
  timestamp: new Date().toISOString()
})

const createMockErrorResponse = (message: string, code = 'ERROR') => ({
  success: false,
  error: { code, message },
  timestamp: new Date().toISOString()
})

3. 在测试用例中配置模拟响应

在测试用例的beforeEach或具体测试中配置模拟响应,支持跨UI包集成:

// 跨UI包集成测试示例:收货地址UI包(包含区域选择组件)
describe('收货地址管理(跨UI包集成)', () => {
  let mockClient: any;

  beforeEach(() => {
    vi.clearAllMocks();

    // 获取模拟的rpcClient实例
    mockClient = mockRpcClient('/');

    // 配置收货地址API响应(收货地址UI包)
    mockClient.index.$get.mockResolvedValue(createMockResponse(200, {
      data: [
        {
          id: 1,
          name: '测试地址',
          phone: '13800138000',
          provinceId: 1,
          cityId: 2,
          districtId: 3,
          detail: '测试街道'
        }
      ],
      pagination: { total: 1, page: 1, pageSize: 10 }
    }));

    mockClient.index.$post.mockResolvedValue(createMockResponse(201, {
      id: 2,
      name: '新地址'
    }));

    mockClient[':id']['$put'].mockResolvedValue(createMockResponse(200));
    mockClient[':id']['$delete'].mockResolvedValue(createMockResponse(204));

    // 配置区域API响应(区域管理UI包 - 跨包支持)
    mockClient.$path('api/areas').$get.mockResolvedValue(createMockResponse(200, {
      data: [
        { id: 1, name: '北京市', code: '110000', level: 1 },
        { id: 2, name: '朝阳区', code: '110105', level: 2, parentId: 1 },
        { id: 3, name: '海淀区', code: '110108', level: 2, parentId: 1 }
      ]
    }));

    mockClient.$path('api/areas/provinces').$get.mockResolvedValue(createMockResponse(200, {
      data: [
        { id: 1, name: '北京市', code: '110000' },
        { id: 2, name: '上海市', code: '310000' }
      ]
    }));

    mockClient.$path('api/areas/:id/cities').$get.mockResolvedValue(createMockResponse(200, {
      data: [
        { id: 2, name: '朝阳区', code: '110105', parentId: 1 },
        { id: 3, name: '海淀区', code: '110108', parentId: 1 }
      ]
    }));
  });

  it('应该显示收货地址列表并支持区域选择', async () => {
    // 测试代码:验证收货地址UI和区域选择组件都能正常工作
    // 所有API调用都通过统一的mockRpcClient模拟
  });

  it('应该处理API错误场景', async () => {
    // 模拟API错误
    mockClient.index.$get.mockRejectedValue(new Error('网络错误'));

    // 测试错误处理
  });
});

管理后台UI包测试策略

1. 模拟范围

  • 统一模拟点: 集中模拟@d8d/shared-ui-components/utils/hc中的rpcClient函数
  • HTTP方法: 支持Hono风格的$get$post$put$delete方法
  • API端点: 支持标准端点(index)、参数化端点(:id)和嵌套路径($path()
  • 响应格式: 模拟完整的Response对象,包含statusokjson()等方法
  • 跨包支持: 天然支持多个UI包组件的API模拟,无需分别模拟客户端管理器

2. 测试设置

  1. 统一模拟: 在每个测试文件顶部使用vi.mock统一模拟rpcClient函数
  2. 测试隔离: 每个测试用例使用独立的模拟实例,在beforeEach中重置
  3. 响应配置: 根据测试场景配置不同的模拟响应(成功、失败、错误等)
  4. 错误测试: 模拟各种错误场景(网络错误、验证错误、权限错误、服务器错误等)
  5. 跨包集成: 支持配置多个UI包的API响应,适用于组件集成测试

3. 最佳实践

  • 统一模拟: 所有API调用都通过模拟rpcClient函数统一拦截
  • 类型安全: 使用TypeScript确保模拟响应与API类型兼容
  • 可维护性: 保持模拟响应与实际API响应结构一致,便于后续更新
  • 文档化: 在测试注释中说明模拟的API行为和预期结果
  • 响应工厂: 创建可重用的模拟响应工厂函数,确保响应格式一致性
  • 跨包考虑: 为集成的UI包组件配置相应的API响应

验证和调试

1. 模拟验证

// 验证API调用次数和参数 - 使用统一模拟的rpcClient
describe('API调用验证(统一模拟)', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('应该验证API调用次数和参数', async () => {
    // 获取模拟的客户端实例
    const mockClient = mockRpcClient('/');

    // 配置模拟响应
    mockClient.index.$get.mockResolvedValue(createMockResponse(200, { data: [] }));
    mockClient.index.$post.mockResolvedValue(createMockResponse(201, { id: 1 }));
    mockClient[':id']['$put'].mockResolvedValue(createMockResponse(200));
    mockClient.$path('api/areas').$get.mockResolvedValue(createMockResponse(200, { data: [] }));

    // 执行测试代码(触发API调用)...

    // 验证API调用次数
    expect(mockClient.index.$get).toHaveBeenCalledTimes(1);
    expect(mockClient.$path('api/areas').$get).toHaveBeenCalledTimes(1);

    // 验证API调用参数
    expect(mockClient.index.$post).toHaveBeenCalledWith({
      json: {
        title: '新广告',
        code: 'new-ad',
        typeId: 1
      }
    });

    // 验证带参数的API调用
    expect(mockClient[':id']['$put']).toHaveBeenCalledWith({
      param: { id: 1 },
      json: {
        title: '更新后的广告'
      }
    });

    // 验证嵌套路径API调用
    expect(mockClient.$path('api/areas').$get).toHaveBeenCalledWith({
      query: { level: 1 }
    });
  });

  it('应该验证错误场景', async () => {
    const mockClient = mockRpcClient('/');

    // 配置错误响应
    mockClient.index.$get.mockRejectedValue(new Error('网络错误'));

    // 执行测试代码...

    // 验证错误调用
    expect(mockClient.index.$get).toHaveBeenCalledTimes(1);
  });
});

2. 调试技巧

  • console.debug: 在测试中使用console.debug输出模拟调用信息,便于调试

    // 在测试中输出调试信息
    console.debug('Mock client calls:', {
    getCalls: mockClient.index.$get.mock.calls,
    postCalls: mockClient.index.$post.mock.calls
    });
    
  • 调用检查: 使用vi.mocked()检查模拟函数的调用参数和次数

    // 检查mockRpcClient的调用
    const mockCalls = vi.mocked(mockRpcClient).mock.calls;
    console.debug('rpcClient调用参数:', mockCalls);
    
    // 检查具体端点调用
    const getCalls = vi.mocked(mockClient.index.$get).mock.calls;
    
  • 响应验证: 确保模拟响应的格式与实际API响应一致

    // 验证响应格式
    const response = await mockClient.index.$get();
    expect(response.status).toBe(200);
    expect(response.ok).toBe(true);
    const data = await response.json();
    expect(data).toHaveProperty('data');
    expect(data).toHaveProperty('pagination');
    
  • 错误模拟: 测试各种错误场景,确保UI能正确处理

    // 模拟不同类型的错误
    mockClient.index.$get.mockRejectedValue(new Error('网络错误')); // 网络错误
    mockClient.index.$get.mockResolvedValue(createMockResponse(500)); // 服务器错误
    mockClient.index.$get.mockResolvedValue(createMockResponse(401)); // 认证错误
    
  • 快照测试: 使用Vitest的快照测试验证UI在不同API响应下的渲染结果

  • 跨包调试: 在跨UI包集成测试中,验证所有相关API都正确配置了模拟响应

测试执行流程

本地开发测试

基础设施包

# 运行所有基础设施包测试
cd packages/shared-types && pnpm test
cd packages/shared-utils && pnpm test
cd packages/shared-crud && pnpm test
cd packages/shared-test-util && pnpm test

# 生成覆盖率报告
cd packages/shared-utils && pnpm test:coverage

业务模块包

# 运行所有业务模块包测试
cd packages/user-module && pnpm test
cd packages/auth-module && pnpm test
cd packages/file-module && pnpm test
cd packages/geo-areas && pnpm test

# 运行单元测试
cd packages/user-module && pnpm test:unit

# 运行集成测试
cd packages/auth-module && pnpm test:integration

# 生成覆盖率报告
cd packages/user-module && pnpm test:coverage

server包

# 运行所有测试
cd packages/server && pnpm test

# 运行集成测试
cd packages/server && pnpm test:integration

# 生成覆盖率报告
cd packages/server && pnpm test:coverage

web应用

# 运行所有测试
cd web && pnpm test

# 运行单元测试
cd web && pnpm test:unit

# 运行集成测试
cd web && pnpm test:integration

# 运行组件测试
cd web && pnpm test:components

# 运行E2E测试
cd web && pnpm test:e2e:chromium

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

CI/CD流水线测试

  1. 代码推送 → 触发测试流水线
  2. 单元测试 → 快速反馈,必须通过
  3. 集成测试 → 验证模块集成,必须通过
  4. E2E测试 → 验证完整流程,建议通过
  5. 覆盖率检查 → 满足最低要求
  6. 测试报告 → 生成详细报告

质量门禁

测试通过标准

  • ✅ 所有单元测试通过
  • ✅ 所有集成测试通过
  • ✅ 关键E2E测试通过
  • ✅ 覆盖率满足最低要求
  • ✅ 无性能回归
  • ✅ 安全测试通过

失败处理流程

  1. 测试失败 → 立即通知开发团队
  2. 分析根本原因 → 确定是测试问题还是代码问题
  3. 优先修复 → 阻塞性问题必须立即修复
  4. 重新测试 → 修复后重新运行测试
  5. 文档更新 → 更新测试策略和案例

安全测试策略

安全测试要求

  • 输入验证测试: 所有API端点必须测试SQL注入、XSS等攻击
  • 认证测试: 测试令牌验证、权限控制
  • 数据保护: 测试敏感数据泄露风险
  • 错误处理: 测试错误信息是否泄露敏感数据

安全测试工具

  • OWASP ZAP: 自动化安全扫描
  • npm audit: 依赖漏洞检查
  • 自定义安全测试: 针对业务逻辑的安全测试

性能测试策略

性能测试要求

  • API响应时间: < 100ms (p95)
  • 数据库查询性能: < 50ms (p95)
  • 并发用户数: 支持100+并发用户
  • 资源使用: CPU < 70%, 内存 < 80%

性能测试工具

  • k6: 负载测试
  • autocannon: API性能测试
  • Playwright: E2E性能监控

测试文档标准

测试代码规范

// 良好的测试示例
describe('UserService', () => {
  describe('createUser()', () => {
    it('应该创建新用户并返回用户对象', async () => {
      // Arrange
      const userData = { username: 'testuser', email: 'test@example.com' };

      // Act
      const result = await userService.createUser(userData);

      // Assert
      expect(result).toHaveProperty('id');
      expect(result.username).toBe('testuser');
      expect(result.email).toBe('test@example.com');
    });

    it('应该拒绝重复的用户名', async () => {
      // Arrange
      const existingUser = await createTestUser({ username: 'existing' });

      // Act & Assert
      await expect(
        userService.createUser({ username: 'existing', email: 'new@example.com' })
      ).rejects.toThrow('用户名已存在');
    });
  });
});

测试命名约定

  • 文件名: [module].test.ts[module].integration.test.ts
  • 描述: 使用「应该...」格式描述测试行为
  • 用例: 明确描述测试场景和预期结果

监控和报告

测试监控指标

  • 测试通过率: > 95%
  • 测试执行时间: < 10分钟(单元+集成)
  • 测试稳定性: 无flaky tests
  • 覆盖率趋势: 持续改进或保持

测试报告要求

  • HTML报告: 详细的覆盖率报告
  • JUnit报告: CI/CD集成
  • 自定义报告: 业务指标测试报告
  • 历史趋势: 测试质量趋势分析

附录

相关文档

工具版本

  • Vitest: 3.2.4
  • Testing Library: 16.3.0
  • Playwright: 1.55.0
  • hono/testing: 内置(Hono 4.8.5)
  • shared-test-util: 1.0.0 (测试基础设施包)
  • TypeORM: 0.3.20 (数据库测试)
  • Redis: 7.0.0 (会话管理测试)

更新日志

日期 版本 描述
2025-11-11 2.8 更新包测试结构,添加模块化包测试策略
2025-11-09 2.7 更新为monorepo测试架构,清理重复测试文件
2025-10-15 2.6 完成遗留测试文件迁移到统一的tests目录结构
2025-10-14 2.5 重构测试文件结构,统一到tests目录
2025-09-20 2.4 更新版本与主架构文档一致
2025-09-19 1.0 初始版本,基于现有测试基础设施

文档状态: 正式版 下次评审: 2025-12-19