news 2026/9/19 21:43:24

OpenMed 测试套件深度指南:基于 pytest 的离线优先单元测试与集成测试实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenMed 测试套件深度指南:基于 pytest 的离线优先单元测试与集成测试实践

OpenMed 测试套件深度指南:基于 pytest 的离线优先单元测试与集成测试实践

【免费下载链接】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 仓库的 tests/README.md 为骨架,系统讲解其测试目录布局、pytest 标记体系、共享 fixture 设计、从源码安装依赖的方法以及全部常用运行命令。OpenMed 是一个本地优先(local-first)的医疗健康 AI SDK,其测试套件的核心设计目标是:在核心依赖安装完成后,完全离线即可执行全部测试——所有下游 Hugging Face 模型 API 均被 mock 替换。读完本文,你将掌握 OpenMed 测试套件的完整结构、如何复现测试环境、如何分层运行单元/集成/慢速/模糊测试,以及如何利用仓库中的共享 fixture 编写自己的新测试。

测试套件总览:为离线执行而设计的目录结构

OpenMed 的tests/目录承载了单元测试与集成测试的完整覆盖。整个套件依赖pytest,并通过大量 mock 屏蔽下游 Hugging Face API,因此只要核心依赖安装完毕,就可以在网络隔离的环境下运行。这一点与 OpenMed"数据不出网、全程本地推理"的产品定位一脉相承——测试环境同样不依赖外部网络。

官方文档定义的核心布局如下:

  • unit/—— 快速、隔离的单元测试,覆盖配置管理、模型加载辅助函数、分词(tokenisation)、格式化与各类工具模块;
  • integration/—— 更高级别的场景测试,通过 mock 的 transformers pipeline 演练公开 API 表面(例如analyze_textlist_models);
  • fixtures/—— 共享的样例文本与可复用的 pytest fixture;
  • conftest.py—— 全局 fixture,负责 mock transformers 组件、配置重置与样例数据。

从仓库实际内容看,套件规模远超这四个目录:tests/unit下有corecliclinicalinteropevalmultimodalriskservicetracestraining等数十个分类目录,累计超过千个测试文件;此外还有fuzz/(基于 Hypothesis 的属性测试与语料回放)、property/(流水线阶段契约测试)、browser/(Playwright 端到端测试)、web/mobile/(Flutter FFI 与 React Native 桥接对比)以及desktop/(Tauri 客户端)。tests/run-tests.sh与根目录 pyproject.toml 中的 pytest 配置把这些测试统一编排进 CI。

从源码安装测试依赖

官方文档给出的安装流程是:创建全新的虚拟环境,以可编辑(editable)模式安装包并附带测试依赖。测试期间 transformer 层会被 patch,但必须仍然可被 import,因此需要安装transformerstorch可选)。

# 从仓库根目录执行 git clone https://gitcode.com/GitHub_Trending/ope/openmed.git cd openmed python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -e . pip install pytest pytest-cov transformers # 可选:为 transformers 安装 CPU 后端 pip install torch --index-url https://download.pytorch.org/whl/cpu

如果要在自定义测试中访问 Hugging Face 的受限(gated)模型,运行 pytest 前需要导出HF_TOKEN;而仓库自带的测试不需要任何网络访问。

一个更省事的替代方案是直接使用dev可选依赖组。根目录 pyproject.toml 中的[project.optional-dependencies].dev已经聚合了pytest>=7.0pytest-cov>=4.0pytest-timeout>=2.3hypothesis>=6.100ruff==0.15.22huggingface-hub>=0.30等全套开发工具,tests/run-tests.sh正是这样做的:

pip install -e '.[dev]'

从源码结构看,OpenMed 还提供了丰富的运行时可选依赖(mlxonnxtorchhfgliner等,见 pyproject.toml),测试时应按被测模块按需安装,例如涉及 ONNX 推理的测试需要pip install -e '.[onnx]'

运行测试:从全量到分层

在项目根目录执行pytest即可运行完整的单元 + 集成套件:

pytest

只运行轻量的单元测试:

pytest tests/unit # 或者通过 marker 过滤 pytest -m "not integration"

只运行集成场景:

pytest -m integration

生成覆盖率报告:

pytest --cov=openmed --cov-report=term-missing

这些命令能够成立,依赖 pyproject.toml 中的[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 会从tests目录收集所有test_*.py/*_test.py文件,并注册了五类 marker:integration(端到端或外部集成)、slow(预期耗时较长的测试)、contract(基于属性的流水线阶段边界契约测试)、fuzz(基于 Hypothesis 的模糊测试)与doctest_examples(针对指定公共模块的 doctest)。例如tests/unit/core/test_result_cache.pytests/unit/core/test_pipeline_latency.py中就同时使用了slow/integration等标记组合。

仓库还提供了编排好的脚本 tests/run-tests.sh,它模拟 CI 的完整流程:

#!/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 . # 不包含慢速标记的核心测试套件(为 zero-shot 模块收集覆盖率) pytest -m "not slow" --cov=openmed/ner --cov-report=term-missing # zero-shot 慢速检查(依赖缺失时优雅跳过) pytest -m slow

脚本展示了三个关键实践:先做代码规范检查(ruff)再跑测试核心套件用-m "not slow"排除慢速测试,同时单独为openmed/ner模块统计覆盖率;慢速测试单独收尾,并在依赖缺失时优雅跳过。

所有测试之所以被设计为可离线运行,正是因为广泛使用了 mock——官方文档明确指出:失败要么指向 OpenMed 代码本身的回归,要么指向本地缺失的依赖,而不是网络问题。

conftest.py 拆解:全局 mock 与状态隔离

测试套件的离线能力集中体现在 tests/conftest.py。它以unittest.mock构造了一整套模拟的 transformers 组件,并用三个autousefixture 保证每个测试之间的全局状态相互隔离。

数据类 fixture

fixture类型说明
sample_configOpenMedConfig构造OpenMedConfig(default_org="TestOrg", cache_dir="/tmp/test_cache", device="cpu", log_level="DEBUG", timeout=60),用于配置相关测试
sample_textstr短医疗文本:"Patient John Doe has diabetes and hypertension. Prescribed metformin 500mg daily."
sample_long_textstr较长的多段临床文书(主诉、现病史、体格检查、实验室结果、评估与计划),覆盖长文本与多句子场景
sample_predictionslist[dict]三条模拟模型预测:B-CONDITION(diabetes, 0.95)、B-MEDICATION(metformin, 0.89)、B-DOSAGE(500mg, 0.87)

模拟组件 fixture

  • mock_tokenizer:一个Mock分词器,tokenize返回["patient", "has", "diabetes"],并提供convert_ids_to_tokensconvert_tokens_to_string;其调用返回值是一个自定义MockEncoding,实现了word_ids()(返回[None, 0, 1, 2, None])并携带input_idsattention_maskoffset_mappingspecial_tokens_mask等标准编码字段——这正是 transformers fast tokenizer 的典型返回结构。
  • mock_model:模拟 token-classification 模型,配置num_labels=3problem_type="token_classification"architectures=["BertForTokenClassification"]
  • mock_pipeline:模拟 HuggingFace pipeline 调用,返回两条实体预测(B-CONDITION/diabetes、B-MEDICATION/metformin,含 score、word、start、end 字段)。
  • mock_model_info:模拟 Hub 模型元信息(modelIdauthordownloadslikeslibrary_nametagspipeline_tag),用于模型注册表 / 搜索相关测试。

自动执行的隔离 fixture

@pytest.fixture(autouse=True) def reset_config(): yield openmed.core.config.set_config(OpenMedConfig()) @pytest.fixture(autouse=True) def reset_tokenizer_cache(): clear_tokenizer_cache() yield clear_tokenizer_cache() @pytest.fixture(autouse=True) def clear_paged_kv_service_env(monkeypatch): for env_var in _PAGED_KV_SERVICE_ENV_VARS: monkeypatch.delenv(env_var, raising=False)

三个autousefixture 分别解决三类全局状态污染问题:

  1. 全局配置重置openmed.core.config维护一个进程级单例_config(见 openmed/core/config.py 的get_config/set_config),若测试之间不重置,前一个测试写入的配置会泄漏到后续测试。reset_config在每次测试结束后恢复为默认OpenMedConfig()
  2. 进程级分词器缓存清空:OpenMed 在 openmed/processing/tokenizer_cache.py 中实现了一个容量 32、线程安全(RLock)的 LRU 分词器缓存。reset_tokenizer_cache在测试前后各调用一次clear_tokenizer_cache(),确保 mock 的分词器与真实缓存互不干扰。
  3. MLX paged KV-cache 环境变量清理_PAGED_KV_SERVICE_ENV_VARS列出的五个OPENMED_SERVICE_MLX_PAGED_KV_CACHE_*环境变量会在测试间被monkeypatch.delenv移除,避免可选的 MLX paged KV-cache 服务配置泄漏进无关测试——这也印证了 OpenMed 的 MLX 推理路径是可选的、环境驱动的。

TestHelpers:面向新测试的构造工具

conftest.py末尾的TestHelpers类(同时以test_helpersfixture 暴露)提供两个静态方法:

  • create_entity_prediction(text, label, confidence, start=None, end=None):基于openmed.processing.outputs.EntityPrediction构造单条实体预测;
  • create_prediction_result(text, entities, model_name="test-model"):把[{"word", "entity", "score", ...}]形式的预测列表转换为完整的PredictionResult(含timestampprocessing_time)。

这使新测试可以直接复用统一的预测数据模型,不必重复手工构造。

集成测试如何驱动真实公共 API

test_end_to_end.py 展示了集成层"mock 底层、驱动真实 API"的写法。以test_analyze_text_full_pipeline为例,它依次 patch 了openmed.core.backends._module_availableopenmed.core.models.HF_AVAILABLEpipelineAutoConfigAutoTokenizerAutoModelForTokenClassification,然后调用公开入口analyze_text(sample_text, model_name="medical-ner", config=...),断言返回结果具备textentitiesmodel_name属性且实体数量正确。

analyze_text的真实实现位于 openmed/init.py,其完整调用链为:validate_inputvalidate_model_nameModelLoader.create_pipeline(内部使用AutoConfig/AutoTokenizer/AutoModelForTokenClassification组装token-classificationpipeline)→ 可选句子切分与分块(chunk)→ner_pipeline(inference_input)→ 边界修正与可选 medical-tokenizer 重映射 →format_predictions输出。集成测试通过 mockAutoConfig/AutoTokenizer/AutoModelForTokenClassification拦截了这条链的模型加载环节,从而在完全无网环境下验证了analyze_text的参数解析、pipeline 组装与结果格式化逻辑。

同文件中的其他测试还覆盖了list_models(mockget_all_models后断言返回模型 id 列表)、包结构(__all__导出完整性)、配置流(get_config/set_config往返)、文本处理管线(TextProcessorclean_text/segment_sentences/extract_medical_entities)、输出格式化(dict/json/html 三种格式)与错误处理(空输入与非法模型名校验抛ValueError)。标记为@pytest.mark.slow的性能类测试则验证长文本处理耗时小于 1 秒。

共享样例数据:fixtures 目录

fixtures/sample_medical_texts.py 提供了更丰富的真实感医疗语料:

  • CLINICAL_NOTE_1/CLINICAL_NOTE_2:完整的结构化临床文书(主诉、既往史、用药、体格检查、评估与计划);
  • MEDICATION_LIST_1/MEDICATION_LIST_2:用药列表(含剂量、频次与给药时间);
  • SHORT_TEXTS:8 条短句(诊断、用药、血压、过敏史、手术史等);
  • PROCEDURE_NOTE/RADIOLOGY_REPORT:操作记录与放射报告;
  • TEST_CASES:带期望实体(CONDITION/MEDICATION/DOSAGE/VITAL_SIGN)的标注用例;
  • EDGE_CASES:空串、纯空白、超长文本、emoji、医学缩写(H/O DM, HTN, CAD)、处方格式(Metformin 500 mg BID x 30 days #30 disp)等边界输入;
  • BATCH_TEST_DATA:批量处理模拟数据。

这些数据覆盖了实体抽取、剂量解析、生命体征识别、缩写消歧、批处理与边界行为等多个测试面,是单元测试复用的主要语料来源。除此之外,tests/fixtures下还有按领域组织的海量 JSON/JSONL 黄金数据(如clinical/pii/i18n/fhir/interop/omop/risk/等),支撑临床关系抽取、多语言 PII、FHIR/OMOP 互操作、差分隐私预算等专项测试。

编写新测试的推荐实践

综合仓库的测试组织方式,为 OpenMed 贡献新测试时可以参考以下模式:

  1. 放到正确的层级:纯逻辑、无模型依赖的测试放tests/unit/对应模块子目录;演练analyze_textdeidentifylist_models等公共 API 的场景放tests/integration/;耗时较长或需要真实资源(如真实句子切分器、容器、模型下载)的测试追加@pytest.mark.integration@pytest.mark.slow;属性化输入用@pytest.mark.fuzz+ Hypothesis。
  2. 优先复用 conftest fixture:医疗文本直接用sample_text/sample_long_text/sample_predictions;需要预测对象用test_helpers.create_prediction_result;配置相关的测试用sample_config,并通过set_config设置、依赖reset_config自动还原。
  3. mock 掉模型与 Hub:参照test_end_to_end.py,用@patch拦截AutoConfig/AutoTokenizer/AutoModelForTokenClassification/pipeline,或直接使用mock_pipeline/mock_tokenizerfixture;涉及模型元信息用mock_model_info
  4. 注意进程级状态:测试不应假设全局配置与分词器缓存是干净的——autouse fixture 已保证隔离,但自建的全局状态也应遵循"用完即清"的对称模式。

排障速查

现象可能原因处理方式
测试因ImportError: HuggingFace transformers is required...失败未安装transformers(即便测试会 mock 它,也必须可 import)pip install transformers,或pip install -e '.[hf]'
集成测试尝试联网缺少HF_TOKEN或测试误触真实 Hub仓库自带测试全部离线;自定义测试如需 gated 模型,先export HF_TOKEN=...
慢速测试拖慢全量运行误把slow测试纳入常规 CIpytest -m "not slow"过滤
覆盖率统计缺失某模块--cov未覆盖目标包参考run-tests.shpytest --cov=openmed/ner --cov-report=term-missing
测试间相互污染进程级配置 / tokenizer 缓存 / 环境变量泄漏依赖conftest.py的 autouse fixture;新全局状态遵循对称清理

结语

OpenMed 的测试套件是一个围绕"离线可复现"精心设计的体系:tests/目录分层清晰、conftest.py 用一套 mock 组件与三个自动清理 fixture 保证了测试的确定性与隔离性,pyproject.toml 中的 marker 体系让单元、集成、慢速、契约与模糊测试可以按需组合,run-tests.sh 则把 lint、覆盖率与分层测试编排成了可一键复现的 CI 流程。无论是复现回归、评估覆盖率,还是为 OpenMed 贡献新测试,本文的命令与 fixture 速查都能让你快速上手。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 21:39:39

前端AI应用开发:流式渲染与状态管理工程实践

1. 这个系列到底在写什么,为什么第四篇才是真正的分水岭“前端手摸手跑路之 AI 应用开发”这个系列,我从第一篇追到第四篇,越看越觉得它踩中了一个很真实的痛点:前端开发者想往 AI 应用方向靠,但市面上要么是纯算法视角…

作者头像 李华
网站建设 2026/9/19 21:37:57

Cursor 跑 MCP Host 接 Server,Base URL 改走 TaoToken 兼容通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 21:36:38

AI代理异常终止分析与解决方案

1. 异常现象解析:Agent terminated due to error最近在调试自动化流程时遇到一个典型报错:"Antigravity提示Agent terminated due to error You can prompt the model to try again or start a"。这个错误通常发生在AI代理执行过程中遇到不可恢…

作者头像 李华
网站建设 2026/9/19 21:35:14

BrewUI:给Homebrew套上图形界面,让Mac包管理更简单

1. 从命令行到界面:BrewUI想解决什么问题如果你是个靠Mac吃饭的开发者,大概率对Homebrew不会陌生。不管是装Node.js、Python,还是拉起MySQL、Redis,一行brew install基本能覆盖绝大多数场景。但问题也出在这里——Homebrew是个典型…

作者头像 李华
网站建设 2026/9/19 21:34:41

哈希表实现电话号码管理系统:从设计到答辩的完整指南

每年到这个时间点,总有人被同一个课程设计题目卡住:哈希表实现电话号码管理系统。这个题在数据结构课设里算是常青树,几乎每届都有人选,可很多人低估了它的难度。哈希函数怎么设计、冲突用什么策略解决、测试数据怎么构造才可信、…

作者头像 李华