测试分层与 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
测试分层的触发策略
- 单元测试:每次 PR 必须运行,目标 <2 分钟完成
- 集成测试:每次合并到主干时运行,允许 5-10 分钟
- E2E 测试:可以只在夜间定时运行,或在 Release 前运行;失败时保存截图方便排查
本章小结
本章核心要点
- 测试分层:单元测试(快,每次 PR)→ 集成测试(中,合并到主干)→ E2E 测试(慢,夜间/发布前);合理分层避免 CI 过慢,同时保证全面覆盖。
- 覆盖率上传:使用 codecov/codecov-action 将 lcov 格式覆盖率上传 Codecov,可在 PR 中自动注释覆盖率变化趋势。
- Service Containers:GitHub Actions 支持在 Job 中启动 Docker 容器(PostgreSQL/Redis/MongoDB 等),services 块中配置健康检查确保服务就绪后再执行测试步骤。
- E2E 测试:Playwright 需要先安装浏览器(playwright install --with-deps);使用 wait-on 等待服务器就绪;if: always() 确保即使测试失败也上传截图和报告。
- PR 注释:使用 actions/github-script 在 PR 中创建/更新注释,将测试结果直接展示在 PR 中;检查是否已有 Bot 注释可避免重复添加。