
本文介绍如何在数值计算类项目中,利用 pytest 随机选取并执行部分“长耗时”单元测试,结合历史耗时统计与分组策略,在有限 ci/cd 时间内最大化测试覆盖率与反馈效率。
在科学计算、数值模拟等场景中,单元测试常因需构建大规模网格(如高分辨率空间/时间离散)或执行长时间数值积分而单测耗时数分钟甚至更久。若每次 Git 提交都全量运行所有长耗时测试(long tests),CI 流水线将不堪重负——可能耗费数十小时,严重拖慢开发节奏。此时,不追求单次全覆盖,而追求多次迭代下的统计性覆盖,是更务实的工程选择。
pytest 本身不原生支持“运行 N 个随机测试”或“限时随机执行后标记剩余为预期失败”,但可通过组合策略高效实现目标:
✅ 推荐实践方案
1. 按耗时分类 + 标签标记
首先使用 @pytest.mark 显式区分测试类型,便于后续筛选:
# test_numerics.py
import pytest
@pytest.mark.fast
def test_small_grid_sum():
assert sum(range(100)) == 4950
@pytest.mark.long
def test_ode_integration_large_grid():
# 模拟:生成 10000×10000 网格并积分 → 耗时数分钟
pass
@pytest.mark.long
def test_pde_solver_convergence():
pass2. 基于历史耗时的智能随机采样(推荐)
核心思路:维护一个轻量级 JSON 数据库(如 test_durations.json),记录每个 @pytest.mark.long 测试的历史平均耗时:
{
"test_ode_integration_large_grid": 217.4,
"test_pde_solver_convergence": 382.1,
"test_fourier_transform_high_res": 156.8
}然后编写脚本 select_long_tests.py,按预算时间(如 7200 秒 = 2 小时)随机贪心选取测试(避免超时):
import json
import random
def select_random_long_tests(budget_sec: float, duration_file: str = "test_durations.json") -> list:
with open(duration_file) as f:
durations = json.load(f)
# 过滤出 long 测试(假设文件名/函数名已知)
long_tests = list(durations.keys())
random.shuffle(long_tests) # 打乱顺序
selected = []
remaining = budget_sec
for test in long_tests:
if durations[test] <= remaining:
selected.append(test)
remaining -= durations[test]
return selected
# 示例:生成 pytest 命令
selected = select_random_long_tests(7200)
print("pytest -m long -k '" + " or ".join(selected) + "'")该脚本可集成进 CI 的 before_script 阶段,动态生成执行命令。
3. 规避重复:轮转排列(Round-robin permutation)
为确保长期迭代下所有长测试被覆盖,可预先生成一个固定随机排列,并按 commit 次数取模轮询:
# 在 CI 中:获取当前 commit 数(或 pipeline ID)作为 seed
import random
commits_since_start = int(os.getenv("CI_PIPELINE_ID", "1")) % 1000
random.seed(commits_since_start)
long_tests = ["test_a", "test_b", "test_c", ...]
random.shuffle(long_tests) # 每次 commit 对应不同 shuffle
selected = long_tests[:5] # 固定取前 5 个4. 关键优化:共享昂贵 setup
若多个长测试共用相同的大规模网格或初始化逻辑,务必提取为 pytest.fixture(scope="session") 或模块级缓存,避免重复开销:
@pytest.fixture(scope="session")
def large_computation_grid():
print("→ Building 10k×10k grid ONCE per session...")
return np.random.random((10000, 10000))
def test_grid_property_1(large_computation_grid):
assert large_computation_grid.mean() > 0.4
def test_grid_property_2(large_computation_grid): # 免费复用!
assert np.linalg.norm(large_computation_grid) > 1e4⚠️ 注意事项与总结
- ❌ 不要依赖 --randomly 插件(如 pytest-randomly)直接随机——它无法控制耗时或数量,且不保证跨 CI 实例的可重现性;
- ✅ 强烈建议分离 CI 任务:short-tests(每次必跑,
- ✅ 将 test_durations.json 纳入版本控制(定期更新),或通过 CI 归档自动更新;
- ✅ 在测试报告中明确标注:“本次执行 long tests: 5/47 (随机采样,预算 2h)”,增强可追溯性;
- ✅ 对于关键路径(如主算法回归),仍建议保留少量“守门员长测试”(guardian tests)每次必跑。
通过以上策略,你既能将 CI 时间控制在合理范围内,又能借助概率论保障:在连续 10 次随机采样(每次 5 个)后,任一特定 long test 未被执行的概率低于 0.005%((42/47)^10 ≈ 0.000047),真正实现高效、可靠、可持续的数值测试治理。










