OpenMed 测试体系实战:测试分层、CI 基线命令与 Zero-Shot 冒烟验证
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
OpenMed 是一个本地优先(local-first)的临床 NER 与 HIPAA PII 脱敏工具库,其测试体系围绕"离线可运行、快速回归、可复现 CI"三个目标构建。本篇基于 docs/testing.md 的官方指引,结合 pyproject.toml 中的 pytest 配置、Makefile 质量门禁与 tests/run-tests.sh 基线脚本,系统讲解 OpenMed 的测试分层(unit / integration / slow)、完整运行命令、GLiNER zero-shot 冒烟检查参数,以及文档与 API 一致性校验方法。读完后你可以直接在本仓库中复现整套测试与冒烟流程,并为新特性补充符合项目规范的测试。
测试体系总览
官方文档 docs/testing.md 的开篇定位很明确:借助现有的测试套件与冒烟脚本,让回归问题远离你的临床工作流。整个测试资产分布在tests/目录下,按 tests/README.md 的说明组织如下:
tests/unit/—— 快速、隔离的单元测试,覆盖配置、模型加载辅助函数、分词、格式化与工具模块;tests/integration/—— 更高层的场景测试,通过 mock 的 transformers pipeline 验证analyze_text、list_models等公共 API 表面;tests/fixtures/—— 共享样例文本与可复用的 pytest fixtures;tests/conftest.py—— 全局 fixture,负责 mock transformers 组件、配置重置与样例数据。
关键设计决策是重度 mock 下游 Hugging Face API:transformer 层在测试期间被打补丁(patched),但transformers包本身仍必须可导入,因此安装时需要pip install transformers(可选安装torch作为 CPU 后端)。这使得捆绑测试完全不需要网络访问即可运行——一旦测试失败,问题要么指向 OpenMed 代码的回归,要么指向本地依赖缺失,排障边界清晰。
测试标记(Marker)分类
docs/testing.md 给出的测试分类表如下:
| 标记 | 位置 | 用途 |
|---|---|---|
unit(默认) | tests/unit/** | 对模型注册表辅助函数、配置工具与核心 API 的快速验证 |
integration | 显式选择(opt-in) | 演练多组件流程(如 pipeline 创建 + formatter) |
slow | 显式选择(opt-in) | 运行较重的 GLiNER 或 Hugging Face 调用;除非传入-m slow否则禁用 |
文档指出这些标记通过 pyproject.toml 中的pytest.ini条目(即[tool.pytest.ini_options])来配置。仓库中该配置块的实际内容比文档表格更丰富,除了文档提到的integration与slow之外,还定义了契约与模糊测试标记,以及测试文件匹配规则与测试路径:
[tool.pytest.ini_options] markers = [ "integration: marks end-to-end or external integration tests", "slow: marks tests that are expected to run slowly", "contract: marks property-based stage-boundary contract tests", "fuzz: marks property-based (Hypothesis) fuzz tests", "doctest_examples: runs doctest examples for targeted public modules" ] python_files = [ "test_*.py", "*_test.py", ] testpaths = [ "tests", ]从源码结构看,标记在测试代码中通过@pytest.mark.<marker>声明。例如 tests/integration/test_end_to_end.py 中的TestEndToEndAnalysis使用@pytest.mark.integration标注,并同时 patchopenmed.core.backends._module_available、openmed.core.models.pipeline、AutoConfig、AutoTokenizer、AutoModelForTokenClassification等 HF 组件,再驱动analyze_text全流程——这正是文档表格中"pipeline 创建 + formatter 组合"类集成流程的真实示例。仓库中同时标注了integration与slow双标记的文件(如 tests/integration/test_distroless_image.py、tests/mobile/test_flutter_ffi.py)则会同时受两层选择条件约束。
运行完整测试套件
docs/testing.md 给出的标准运行流程是:
uv pip install ".[dev,hf]" make lint make format-check make lint-swift # 用于 Swift/OpenMedKit 变更 pytest # 快速的 unit/integration 混合 pytest -m "not slow" # 默认行为 pytest -m slow # 仅运行长耗时用例这些命令与 Makefile 中的目标一一对应,理解各目标的具体实现有助于排查失败:
make lint—— 实际执行uv run --frozen --extra dev ruff check .,即基于锁定的 uv 开发环境运行 Ruff 静态检查;make format-check—— 执行uv run --frozen --extra dev ruff format --check .,只检查不修改文件;make lint-swift—— 调用 scripts/lint_swift.sh,对swift/OpenMedKit等 Swift 侧代码做格式 lint,仅在你改动了 Swift/OpenMedKit 时需要;make test—— 执行uv run --frozen --extra dev pytest,与裸pytest等效但复现性更强(锁定依赖解析);make quality—— 组合门禁lint type-check format-check test,适合作为本地提交前的完整质量关卡;- 另外,pyproject.toml 中 Ruff 的 lint 规则为
E9、F63、F7、F82、I(目标版本py310,行宽 88,并排除examples/notebooks与openmed/service/proto/generated)。
tests/run-tests.sh:CI 基线脚本
docs/testing.md 特别强调:tests/run-tests.sh把 lint、格式检查、单元测试与慢速冒烟检查串在一起,应作为 CI 的基线。查看 tests/run-tests.sh 的完整实现,它按以下顺序执行:
#!/usr/bin/env bash set -euo pipefail python3 -m venv .venv source .venv/bin/activate python -m pip install --upgrade pip >/tmp/pip-up.log pip install -e '.[dev]' >/tmp/pip-install.log ruff check . ruff format --check . # Core test suite without slow markers (collect coverage for zero-shot modules) pytest -m "not slow" --cov=openmed/ner --cov-report=term-missing # Zero-shot slow checks (gracefully skip when dependencies unavailable) pytest -m slow其中有三个值得注意的细节:
set -euo pipefail保证任何一步失败立即终止,避免"假绿";- 核心套件显式指定
--cov=openmed/ner,即对 zero-shot NER 模块单独收集覆盖率(终端缺失行报告),这与 OpenMed 将 zero-shot 能力列为核心特性之一的定位一致; pytest -m slow位于脚本末尾,慢速检查在依赖不可用时优雅跳过(gracefully skip),不会阻塞主流程。
若不需要脚本、只想手动分层运行,tests/README.md 给出了等效命令:
# 全量(unit + integration) pytest # 仅轻量单元测试 pytest tests/unit pytest -m "not integration" # 仅集成场景 pytest -m integration # 生成覆盖率报告 pytest --cov=openmed --cov-report=term-missingGLiNER Zero-Shot 冒烟检查
对于 zero-shot 场景,docs/testing.md 建议的冒烟命令是:
uv pip install ".[gliner]" python scripts/smoke_gliner.py --limit 2 --threshold 0.4 --adapterscripts/smoke_gliner.py 是一个独立的命令行冒烟运行器,其参数与行为在源码中都有明确定义,可结合参数注释逐一理解:
| 参数 | 默认值 | 作用 |
|---|---|---|
--index | 打包内置索引 | 指定models/index.json的路径,缺省时使用包内模型索引 |
--limit | 3 | 限制被测 GLiNER 模型数量 |
--threshold | 0.4 | 实体置信度阈值 |
--adapter | 关闭(flag) | 推理后运行 token classification 适配器,输出 BIO/BILOU 标签 |
脚本的源码级执行链路为:先通过is_gliner_available()检查依赖,缺失时直接报错并提示pip install .[gliner];然后load_index()加载模型索引并筛选出family == ModelFamily.GLINER的记录;对每个被选中的模型,按其第一个 domain 从DEFAULT_SAMPLE_TEXTS中选取领域样例文本(覆盖 biomedical、clinical、genomic、finance、legal 等 15 个领域,如 biomedical 域的 "Imatinib inhibits BCR-ABL in chronic myeloid leukemia patients."),构造NerRequest(model_id=..., text=..., threshold=..., domain=...)后调用infer()执行推理;每个实体以- <label>: '<text>' [start-end] score=<score>的格式打印。若传入--adapter,再调用to_token_classification(response.entities, request.text)把实体结果转换为 token 级标注(TokenClassificationResult),逐 token 打印标签——这条链路同时验证了推理与 BIO/BILOU 转换两个环节,正好对应后文"覆盖建议"中提到的 zero-shot 适配器测试点。
文档与 API 一致性检查
docs/testing.md 还给出两类"套件之外"的检查:
1. 文档构建检查——在 CI 中加入uv run mkdocs build --strict,让缺失的导航条目、重复锚点或损坏的 markdown 直接导致构建失败。仓库中 Makefile 的docs-serve(本地 127.0.0.1:8008 热重载)与docs-build/docs-stage目标(构建并暂存最终 Pages 产物到site/)提供了本地对照手段。
2. 轻量 API 冒烟测试——确保打包与 extras 保持同步的最小示例:
from openmed import analyze_text result = analyze_text("QA ping", model_name="disease_detection_superclinical") assert result.entities is not None这个 snippet 的价值在于:它同时验证了打包完整性(openmed可导入)、模型注册表条目(disease_detection_superclinical是合法model_name)与核心 API 返回结构(result.entities非空)。
新增功能时的覆盖建议清单
docs/testing.md 结尾给出的四项覆盖建议,结合仓库现有测试资产可以落地为具体做法:
- 模型注册表条目——确保新模型键出现在
list_model_categories中。可在tests/unit下补充断言新键的分类归属,参照 tests/integration/test_end_to_end.py 中对list_models的调用方式; - 公共 API 选项与默认行为——验证
analyze_text等入口的参数默认值与行为契约; - Formatter 行为(HTML/CSS 属性、元数据传播)——单元测试 formatter 的输出属性与元数据透传;
- Zero-shot 适配器(BIO/BILOU 转换)——即
scripts/smoke_gliner.py中--adapter路径所覆盖的to_token_classification转换逻辑。
编写这些测试时可直接复用 tests/conftest.py 提供的全局 fixture,例如:
sample_text/sample_long_text—— 短、长两档临床样例文本;mock_pipeline/sample_predictions—— 预置了B-CONDITION(diabetes, score 0.95)、B-MEDICATION(metformin, score 0.89)、B-DOSAGE(500mg)等 BIO 标签预测结果的 mock pipeline,使 NER 后处理逻辑可以完全离线断言;mock_tokenizer—— 带offset_mapping、word_ids()的 mock tokenizer,用于验证 span 对齐;sample_config—— 基于OpenMedConfig的测试配置(device="cpu"、缓存目录指向/tmp)。
小结
OpenMed 的测试实践可以浓缩为一条本地工作流:
# 安装开发依赖并运行质量门禁 make quality # = lint + type-check + format-check + test # 或以 CI 基线脚本方式一键执行(含 zero-shot 覆盖率) bash tests/run-tests.sh # zero-shot 冒烟(需要 [gliner] extras) uv pip install ".[gliner]" python scripts/smoke_gliner.py --limit 2 --threshold 0.4 --adapter遵循 docs/testing.md 的分类表、tests/run-tests.sh 的 CI 基线以及上文覆盖建议清单,可以让新增代码的文档保持准确、自动化流水线保持绿色,这也是该项目"离线优先、可复现"工程哲学在质量保障层面的直接体现。
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考