
pytest 在 jenkins 环境中跳过参数化测试,根本原因在于测试收集阶段(collection phase)早于工作区资源就绪,而 jenkins 清理工作区导致 `@pytest.mark.parametrize` 中调用的 `get_asset()` 提前返回空列表;需将动态资产发现逻辑移至 `pytest_sessionstart` 等会话级钩子中。
在使用 pytest 进行参数化测试时,若测试函数依赖运行时动态生成的参数(如从文件系统读取的测试资产),极易在 CI 环境(尤其是 Jenkins)中出现「本地能跑、Jenkins 跳过」的诡异现象。其本质并非 Jenkins 本身限制,而是 pytest 的测试收集机制与CI 工作流时序发生冲突所致。
? 问题根源解析
pytest 在执行任何测试前,会先进入 collection 阶段:静态扫描所有测试模块,解析 @pytest.mark.parametrize、@pytest.fixture 等装饰器,并立即求值其中的参数表达式(如 get_asset())。此时:
- 若 get_asset() 依赖磁盘上的 asset/ 目录,则该目录必须在 collection 阶段已存在且可访问;
- Jenkins 默认启用 “Delete workspace before build starts”,导致每次构建开始时工作区为空;
- 因此 collection 阶段调用 get_asset() 返回空列表 → pytest 认为无参数可迭代 → 整个 test_app_launch_asset 被静默跳过(显示为 skipped 或甚至不显示);
- 而手动登录 Jenkins 机器后执行命令时,工作区已被前次构建残留的 asset/ 填充,故 get_asset() 正常返回 → 测试正常执行。
⚠️ 注意:这不是 get_asset 函数逻辑错误,而是执行时机错配——它被当作“编译期常量”求值,实则应是“运行期动态数据”。
✅ 正确解决方案:使用 pytest 会话级钩子预加载参数
应避免在 @parametrize 中直接调用 IO 密集型函数。推荐将资产发现逻辑提前至 pytest_sessionstart(在 collection 之前执行),并将结果缓存到 config 对象中供后续使用:
# conftest.py
import pytest
from pathlib import Path
def get_asset() -> list[Path]:
"""安全版资产发现:确保路径存在且可读"""
asset_dir = Path(__file__).parent / 'asset'
if not asset_dir.exists():
return []
return [
p for p in asset_dir.iterdir()
if p.is_file() and 'need_to_skip_asset' not in p.name
]
def pytest_sessionstart(session):
"""在测试收集前执行:预加载资产列表并挂载到配置"""
assets = get_asset()
session.config._metadata['available_assets'] = assets # 可选:用于报告
# 将资产列表注入全局变量或 session 属性(推荐)
session.assets = assets
# 在测试文件中改写参数化逻辑
@pytest.fixture(scope='session', autouse=True)
def available_assets(request):
"""提供会话级 fixture,确保资产列表在测试中可用"""
return request.session.assets
@pytest.mark.parametrize('asset', [], indirect=True) # 占位符,实际由 fixture 提供
def test_app_launch_asset(app_binary, asset, available_assets):
"""实际测试逻辑 —— 参数由 fixture 动态注入"""
print(f'Application: {app_binary}')
print(f'Asset: {asset}')
applib.execute(
cmd=[str(app_binary), str(asset)],
timeout=15,
)但更简洁、符合 pytest 惯例的方式是:完全弃用 @parametrize 的函数调用形式,改用 indirect + fixture 组合:
# test_app.py
import pytest
@pytest.fixture(params=[]) # 空占位,真实参数由 conftest.py 注入
def asset(request):
# 此处可访问 session.assets(需在 conftest.py 中设置)
session = request.session
if not hasattr(session, 'assets'):
pytest.skip("No assets found — check asset directory existence")
return session.assets[request.param]
# 重写 parametrize:传入索引而非对象
@pytest.mark.parametrize('asset', list(range(100)), indirect=True)
def test_app_launch_asset(app_binary, asset):
print(f'Application: {app_binary}')
print(f'Asset: {asset}')
applib.execute(cmd=[str(app_binary), str(asset)], timeout=15)不过最推荐的工业级实践是:在 conftest.py 中定义一个 session-scoped fixture,返回完整资产列表,再在测试中通过 for 循环显式遍历(牺牲少量 pytest 原生参数化语法糖,换取完全可控性):
# conftest.py
import pytest
from pathlib import Path
@pytest.fixture(scope='session')
def all_assets():
asset_dir = Path(__file__).parent / 'asset'
if not asset_dir.exists():
pytest.skip(f"Asset directory missing: {asset_dir}")
return [
p for p in asset_dir.iterdir()
if p.is_file() and 'need_to_skip_asset' not in p.name
]
# test_app.py
def test_app_launch_asset(app_binary, all_assets):
"""单测试函数内遍历所有资产 —— 完全规避 collection 时序问题"""
for asset in all_assets:
print(f'Running on asset: {asset}')
applib.execute(cmd=[str(app_binary), str(asset)], timeout=15)? 关键注意事项
- ✅ 永远不要在 @pytest.mark.parametrize(...) 的参数表达式中执行 IO 操作(如读文件、查数据库、调用外部命令);
- ✅ pytest_sessionstart 和 pytest_configure 是仅有的两个在 collection 之前触发的钩子,适合做预热准备;
- ✅ Jenkins 构建日志中若看到 collected 0 items,基本可断定 collection 阶段参数源为空;
- ✅ 在 conftest.py 中添加 print() 或日志输出,验证钩子是否被触发(注意 Jenkins 控制台编码与缓冲);
- ✅ 本地调试时,可临时在 get_asset() 开头加入 assert Path('asset').exists(),快速暴露环境差异。
通过将动态数据获取逻辑与 pytest 的生命周期对齐,即可彻底解决 Jenkins 下测试“神秘跳过”的问题,让 CI 行为与本地开发保持一致、可预测、可调试。










