测试覆盖率
测试覆盖率是衡量测试充分性的重要指标,它表示测试代码执行了多少生产代码。理解和使用覆盖率可以帮助识别测试盲区,提高代码质量。
什么是测试覆盖率?
测试覆盖率量化了测试执行过程中代码被覆盖的程度。它回答了「我们的测试有多全面」这个问题。
覆盖率的意义
| 作用 | 说明 |
|---|---|
| 发现测试盲区 | 识别未被测试的代码 |
| 评估测试质量 | 衡量测试的充分性 |
| 指导测试编写 | 优先覆盖关键代码 |
| 质量门槛 | 作为 CI/CD 的质量门禁 |
覆盖率的局限性
覆盖率数字本身并不能保证测试质量:
- 高覆盖率 ≠ 高质量:测试可能只是执行了代码,没有验证结果
- 盲追覆盖率有害:为了数字而写测试,忽视测试本质
- 不测试逻辑正确性:覆盖率只关心代码是否执行
# ❌ 高覆盖率但无意义的测试
def add(a, b):
return a + b
def test_add(): # 覆盖率 100%
add(1, 2) # 但没有断言!
# ✅ 有意义的测试
def test_add_returns_sum():
result = add(1, 2)
assert result == 3 # 有验证
覆盖率类型
主要覆盖率类型
| 类型 | 说明 | 计算方式 |
|---|---|---|
| 语句覆盖 | 每条语句是否执行 | 已执行语句数 / 总语句数 |
| 分支覆盖 | 每个分支是否执行 | 已执行分支数 / 总分支数 |
| 函数覆盖 | 每个函数是否调用 | 已调用函数数 / 总函数数 |
| 行覆盖 | 每行代码是否执行 | 已执行行数 / 总行数 |
| 条件覆盖 | 每个条件是否为真/假 | 已覆盖条件数 / 总条件数 |
语句覆盖(Statement Coverage)
测量代码中每个可执行语句是否被执行。
def divide(a, b):
if b == 0:
return None # 语句1
return a / b # 语句2
# 测试用例1:divide(10, 2)
# 执行语句2,覆盖率 50%
# 测试用例2:divide(10, 0)
# 执行语句1,覆盖率 50%
# 两个测试一起,语句覆盖率 100%
分支覆盖(Branch Coverage)
测量每个判断分支是否被执行。
def classify(age):
if age < 0: # 分支1: true/false
return "invalid"
elif age < 18: # 分支2: true/false
return "minor"
else: # 分支3
return "adult"
# 分支覆盖需要测试:
# 1. age < 0 (true) -> "invalid"
# 2. age >= 0 and age < 18 -> "minor"
# 3. age >= 18 -> "adult"
def test_classify():
assert classify(-1) == "invalid" # 分支1 true
assert classify(10) == "minor" # 分支1 false, 分支2 true
assert classify(20) == "adult" # 分支1 false, 分支2 false
# 分支覆盖率:100%(6个分支全部覆盖)
条件覆盖(Condition Coverage)
测量每个布尔子表达式的真假值。
def can_vote(age, is_citizen):
# 两个条件:age >= 18, is_citizen
if age >= 18 and is_citizen:
return True
return False
# 条件覆盖需要:
# age >= 18: true 和 false
# is_citizen: true 和 false
def test_can_vote():
assert can_vote(20, True) == True # age>=18=true, citizen=true
assert can_vote(16, True) == False # age>=18=false, citizen=true
assert can_vote(20, False) == False # age>=18=true, citizen=false
# 条件覆盖率:100%
路径覆盖(Path Coverage)
测量所有可能的执行路径。
def process(a, b):
if a > 0:
if b > 0:
return "both positive" # 路径1
else:
return "a positive" # 路径2
else:
return "a not positive" # 路径3
# 路径覆盖测试
def test_process_paths():
assert process(1, 1) == "both positive" # 路径1
assert process(1, -1) == "a positive" # 路径2
assert process(-1, 1) == "a not positive" # 路径3
覆盖率工具
Python - pytest-cov
# 安装
pip install pytest-cov
# 运行测试并生成覆盖率报告
pytest --cov=myapp tests/
# 指定报告格式
pytest --cov=myapp --cov-report=html tests/
pytest --cov=myapp --cov-report=xml tests/
pytest --cov=myapp --cov-report=term-missing tests/
# 设置覆盖率阈值
pytest --cov=myapp --cov-fail-under=80 tests/
配置文件
# pytest.ini
[pytest]
addopts =
--cov=myapp
--cov-report=term-missing
--cov-report=html
--cov-fail-under=80
testpaths = tests
# .coveragerc
[run]
source = myapp
omit =
*/tests/*
*/__pycache__/*
*/migrations/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
覆盖率报告示例
Name Stmts Miss Cover Missing
----------------------------------------------------------------------
myapp/__init__.py 1 0 100%
myapp/models.py 45 2 96% 23-24
myapp/services.py 89 15 83% 45-60, 78-82
myapp/api.py 120 10 92% 156-165
----------------------------------------------------------------------
TOTAL 255 27 89%
JavaScript - Jest / Istanbul
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/**/*.spec.js',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
# 运行测试并生成覆盖率
jest --coverage
# 只运行特定文件的覆盖率
jest --coverage --collectCoverageFrom='src/**/*.js'
# 输出格式
jest --coverage --coverageReporters=text
jest --coverage --coverageReporters=html
Java - JaCoCo
<!-- pom.xml -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
# 运行测试并生成报告
mvn test
# 查看报告
# target/site/jacoco/index.html
Go - go test -cover
# 基本覆盖率
go test -cover ./...
# 生成覆盖率文件
go test -coverprofile=coverage.out ./...
# 查看覆盖率详情
go tool cover -func=coverage.out
# 生成 HTML 报告
go tool cover -html=coverage.out -o coverage.html
# 按函数显示覆盖率
go tool cover -func=coverage.out | grep total
// 排除特定代码
func someFunction() {
// 省略覆盖率检查的代码
// 空函数体不计数
}
覆盖率目标设定
合理的覆盖率目标
| 代码类型 | 建议覆盖率 | 说明 |
|---|---|---|
| 核心业务逻辑 | 90%+ | 关键路径必须充分测试 |
| 通用工具类 | 80%+ | 被多处使用,需要稳定 |
| API 层 | 70-80% | 关注接口契约 |
| UI 组件 | 60-70% | 关注交互行为 |
| 配置/启动代码 | 可忽略 | 通常难以测试,风险低 |
不同项目的目标
# 覆盖率配置示例
coverage_targets:
critical_systems: # 金融、医疗等
line: 90
branch: 85
function: 95
standard_projects: # 一般业务项目
line: 80
branch: 70
function: 80
prototypes: # 原型/实验项目
line: 60
branch: 50
function: 60
提高覆盖率的方法
1. 识别未覆盖代码
# 使用 term-missing 查看未覆盖的行
pytest --cov=myapp --cov-report=term-missing
# 输出示例:
# myapp/services.py 45-60, 78-82
2. 分析未覆盖原因
| 原因 | 解决方案 |
|---|---|
| 缺少测试用例 | 添加测试 |
| 异常处理分支 | 模拟异常场景 |
| 防御性代码 | 评估是否必要 |
| 死代码 | 删除无用代码 |
| 条件组合复杂 | 简化或拆分逻辑 |
3. 编写针对性测试
# 发现未覆盖的异常处理
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零") # 未覆盖
return a / b
# 添加异常测试
def test_divide_by_zero():
with pytest.raises(ValueError, match="除数不能为零"):
divide(10, 0)
4. 使用参数化减少重复
import pytest
# 参数化测试,提高覆盖率效率
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("World", "WORLD"),
("", ""),
("123", "123"),
("Hello World", "HELLO WORLD"),
])
def test_to_uppercase(input, expected):
assert input.upper() == expected
覆盖率排除
有时需要排除某些代码的覆盖率计算:
Python
# .coveragerc
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
# 代码中使用
def debug_only():
if settings.DEBUG: # pragma: no cover
print("debug info")
JavaScript
// 使用 istanbul ignore
function debugOnly() {
/* istanbul ignore next */
if (process.env.DEBUG) {
console.log('debug info');
}
}
Java
// 使用 @Generated 或配置排除
@Generated("generated by tool")
public void generatedMethod() {
// ...
}
CI/CD 集成
GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest-cov
- name: Run tests with coverage
run: pytest --cov=myapp --cov-fail-under=80 --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
fail_ci_if_error: true
GitLab CI
# .gitlab-ci.yml
test:
stage: test
script:
- pip install pytest-cov
- pytest --cov=myapp --cov-report=xml
coverage: '/TOTAL.*\s+(\d+%)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
覆盖率最佳实践
1. 关注质量而非数字
# ❌ 追求数字的无效测试
def test_user():
user = User("张三", 25)
assert user is not None # 只验证对象存在
# ✅ 有意义的测试
def test_user_creation():
"""测试用户创建"""
user = User("张三", 25)
assert user.name == "张三"
assert user.age == 25
assert user.is_active is True
# 测试边界条件
with pytest.raises(ValueError):
User("", 25) # 空名字
with pytest.raises(ValueError):
User("张三", -1) # 负年龄
2. 优先覆盖关键代码
# 核心业务逻辑优先
# 支付处理、数据验证、权限检查等
def test_payment_processing():
"""核心支付逻辑必须充分测试"""
# 正常流程
result = process_payment(100, "USD")
assert result.status == "success"
# 边界情况
result = process_payment(0, "USD")
assert result.status == "invalid"
# 异常情况
with pytest.raises(PaymentError):
process_payment(-100, "USD")
3. 定期审查覆盖率报告
# 设置定期的覆盖率报告审查
# 每周或每个迭代审查覆盖率变化
pytest --cov=myapp --cov-report=html
# 打开 htmlcov/index.html 审查
4. 覆盖率趋势监控
# 使用 codecov 或类似服务监控趋势
# 在 PR 中自动评论覆盖率变化
# 设置覆盖率下降阈值
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: 80%
threshold: 1% # 允许下降1%
总结
测试覆盖率是测试质量的重要指标,但不是唯一指标。关键要点:
- 合理目标:根据项目类型设定合理目标
- 质量优先:测试质量比覆盖率数字更重要
- 持续监控:将覆盖率检查纳入 CI/CD
- 关注盲区:利用覆盖率发现测试盲区
- 迭代改进:持续提高测试覆盖率