Chapter 08

测试自动化:单元/集成/E2E

将测试无缝集成到 CI 流水线,自动生成覆盖率报告,用测试结果注释 PR

测试分层与 CI 策略

在 CI 流水线中,不同层次的测试有不同的特性和运行策略。合理分层可以在快速反馈和全面覆盖之间取得平衡:

单元测试(Unit Tests)
测试单个函数/模块,不依赖外部服务,速度最快(秒级)。每次 PR 必须运行,目标在 2 分钟内完成。测试框架:Jest(JS)、pytest(Python)、go test(Go)、JUnit(Java)。
集成测试(Integration Tests)
测试模块间的交互,通常需要数据库或外部服务(可用 Docker 容器)。在 PR 或合并到主干时运行,允许 5-10 分钟。需要设置 Service Containers(GitHub Actions 支持 Docker Compose 风格的 services 字段)。
E2E 测试(End-to-End Tests)
模拟真实用户操作,需要启动完整应用。时间最长(10-30 分钟),通常在夜间定时运行或仅在 Release 分支触发。工具:Playwright、Cypress、Selenium。
覆盖率(Coverage)
衡量测试对代码的覆盖程度。常见格式:lcov(行覆盖率)、cobertura(分支覆盖率)。Codecov/Coveralls 等服务可接收覆盖率报告并在 PR 中展示趋势。

JavaScript/TypeScript(Jest)

name: JavaScript Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      # 运行 Jest 测试并生成覆盖率
      - name: 运行测试
        run: npm test -- --coverage --coverageReporters=lcov,json-summary
        env:
          CI: true

      # 上传覆盖率到 Codecov
      - name: 上传覆盖率
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
          fail_ci_if_error: true

      # 在 PR 中添加覆盖率注释
      - name: 覆盖率注释 PR
        uses: romeovs/lcov-reporter-action@v0.3.1
        if: github.event_name == 'pull_request'
        with:
          lcov-file: ./coverage/lcov.info
          github-token: ${{ secrets.GITHUB_TOKEN }}

Python(pytest)

jobs:
  test-python:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
      - run: pip install -r requirements.txt pytest pytest-cov

      - name: 运行 pytest
        run: |
          pytest tests/ \
            --cov=src \
            --cov-report=xml \
            --cov-report=html \
            --junit-xml=test-results.xml \
            -v

      # JUnit 格式测试报告(GitHub Actions 原生支持)
      - name: 发布测试结果
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Python Tests
          path: test-results.xml
          reporter: java-junit

Go 测试

jobs:
  test-go:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
          cache: true

      - name: 运行 Go 测试
        run: |
          go test ./... \
            -race \
            -coverprofile=coverage.out \
            -covermode=atomic

      - name: 显示覆盖率
        run: go tool cover -func=coverage.out

E2E 测试(Playwright)

jobs:
  e2e-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      # 安装 Playwright 浏览器
      - name: 安装 Playwright 浏览器
        run: npx playwright install --with-deps chromium firefox

      # 启动应用服务器
      - name: 启动应用
        run: npm start &
        env:
          PORT: 3000

      - name: 等待服务器就绪
        run: npx wait-on http://localhost:3000

      # 运行 E2E 测试
      - name: 运行 Playwright 测试
        run: npx playwright test
        env:
          BASE_URL: http://localhost:3000

      # 上传测试报告和截图(失败时)
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: |
            playwright-report/
            test-results/
          retention-days: 14

在 PR 上添加测试状态注释

      # 测试失败时,自动在 PR 中添加注释说明失败原因
      - name: PR 注释:测试失败
        uses: actions/github-script@v7
        if: failure() && github.event_name == 'pull_request'
        with:
          script: |
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });

            const botComment = comments.find(c =>
              c.user.type === 'Bot' && c.body.includes('CI 测试结果')
            );

            const body = `## CI 测试结果 ❌

            测试在 \`${{ github.sha }}\` 上失败。

            查看 [完整日志](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) 了解详情。`;

            if (botComment) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: botComment.id,
                body
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body
              });
            }

集成测试:使用 Service Containers

jobs:
  integration-test:
    runs-on: ubuntu-latest

    # Service Containers:在 Job 开始前启动 Docker 容器
    services:
      # 启动 PostgreSQL(供集成测试使用)
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
        ports:
          - 5432:5432              # 映射到宿主机端口
        options: >-                # 健康检查:确保数据库就绪后才继续
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      # 启动 Redis(用于缓存测试)
      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      # 运行数据库迁移
      - name: 运行数据库迁移
        run: npm run db:migrate
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb

      # 运行集成测试
      - name: 运行集成测试
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
测试分层的触发策略

本章小结

本章核心要点