Opik Python SDK 测试实战指南:从 fake_backend 集成测试到 E2E 验证器
2026/9/13 17:53:22 网站建设 项目流程

Opik Python SDK 测试实战指南:从 fake_backend 集成测试到 E2E 验证器

【免费下载链接】comet-llmDebug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards.项目地址: https://gitcode.com/GitHub_Trending/co/comet-llm

本文是 Opik(comet-llm 仓库)Python SDK 的测试模式完整指南。你将从零掌握仓库内 Python SDK 的三层测试体系:基于fake_backend的集成测试、基于verifiers的端到端(E2E)测试,以及tests.testlib提供的断言与匹配工具;同时学会在本地以 CI 等价方式或 dev-runner 方式运行tests/e2e全量套件,并快速定位失败用例。读完本文,你可以为 Opik Python SDK 的新功能写出与仓库现有测试风格一致、可稳定运行的高质量用例。

测试命名规范:让测试意图自解释

仓库要求每个测试函数遵循三段式命名模式(见 .agents/skills/python-sdk/testing.md):

# 模式:test_WHAT__CASE_DESCRIPTION__EXPECTED_RESULT def test_tracked_function__error_inside_inner_function__caught_in_top_level_span(): pass # 正常路径:test_WHAT__happyflow def test_optimization_lifecycle__happyflow(): pass
  • WHAT:被测对象或行为,例如tracked_functionoptimization_lifecycle
  • CASE_DESCRIPTION:具体场景,例如error_inside_inner_function
  • EXPECTED_RESULT:期望结果,例如caught_in_top_level_span
  • 正常路径统一使用happyflow后缀,与仓库中大量test_*__happyflow用例保持一致性。

这种命名规范让测试失败时的输出直接可读,也方便通过pytest -k按关键词筛选相关用例。

用 fake_backend 编写集成测试

fake_backend是仓库集成测试的核心设施,适用于一切会产生 trace/span 的测试(尤其是第三方框架集成测试,例如sdks/python/tests/library_integration/下的所有用例)。它不发起真实网络请求,而是把 SDK 内部消息管线替换为内存中的后端模拟器。

fake_backend 的工作原理

从 sdks/python/tests/conftest.py 的 fixture 源码可以看到:

@pytest.fixture def fake_backend(patch_streamer): """ Patches the function that creates an instance of Streamer under the hood of Opik. As a result, instead of sending data to the backend, it's being passed to the backend emulator, which uses this data to build span and trace trees. """ streamer, fake_message_processor_ = patch_streamer ... mock_construct_online_streamer = mock.Mock() mock_construct_online_streamer.return_value = streamer with mock.patch.object( streamer_constructors, "construct_online_streamer", mock_construct_online_streamer, ): yield fake_message_processor_

即:通过mock.patch替换streamer_constructors.construct_online_streamer,让 SDK 以为连接了真实后端,实际把数据交给BackendEmulatorMessageProcessor(见 sdks/python/tests/testlib/backend_emulator_message_processor.py)。该处理器继承自opik.message_processing.emulation.EmulatorMessageProcessor,把收到的每条消息重建成TraceModel/SpanModel树,测试可通过fake_backend.trace_treesfake_backend.span_trees访问。

配套 fixture 还有两个变体(见 sdks/python/tests/conftest.py):

  • fake_backend_without_batching:当测试涉及 Span/Trace 的 update 请求(不支持批处理)时使用;
  • fake_backend_with_patched_environment:以request.param指定的环境变量覆盖配合 fake_backend 使用,适合验证环境变量对行为的影响。

期望模型:TraceModel / SpanModel

测试中用于比对期望值的TraceModel/SpanModel定义在 sdks/python/tests/testlib/models.py。它们为默认值做了精心设计,测试无需重复声明不关心的字段:

字段默认值含义
project_nameANY默认不校验项目名,除非测试显式指定
last_updated_atANY_BUT_NONE只断言“不为 None”,不比较具体时间
attachmentsANY默认不校验附件
source"sdk"默认来源标记

完整示例:嵌套函数追踪

from tests.testlib import TraceModel, SpanModel, ANY_BUT_NONE, assert_equal from opik.decorator import tracker def test_track__one_nested_function__happyflow(fake_backend): @tracker.track def f_inner(x): return "inner-output" @tracker.track def f_outer(x): f_inner("inner-input") return "outer-output" f_outer("outer-input") tracker.flush_tracker() EXPECTED_TRACE_TREE = TraceModel( id=ANY_BUT_NONE, name="f_outer", input={"x": "outer-input"}, output={"output": "outer-output"}, start_time=ANY_BUT_NONE, end_time=ANY_BUT_NONE, spans=[ SpanModel( id=ANY_BUT_NONE, name="f_outer", input={"x": "outer-input"}, output={"output": "outer-output"}, spans=[ SpanModel( id=ANY_BUT_NONE, name="f_inner", input={"x": "inner-input"}, output={"output": "inner-output"}, spans=[], ) ], ) ], ) assert len(fake_backend.trace_trees) == 1 assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])

要点解读:

  • @tracker.track装饰器同时装饰内、外两层函数,验证嵌套 span 树的构建;
  • 测试结束时调用tracker.flush_tracker(),确保内存中的消息被模拟器处理完成;
  • fake_backend.trace_trees返回List[TraceModel],期望树以递归spans列表表达层级关系;
  • assert_equal支持ANY_*通配符,因此id、时间戳等动态字段用ANY_BUT_NONE占位。

testlib 工具集:ANY 系列与断言助手

ANY 系列匹配器

tests.testlib提供了四种通配匹配器,实现在 sdks/python/tests/testlib/any_compare_helpers.py:

from tests.testlib import ANY_BUT_NONE, ANY_STRING, assert_equal # ANY_BUT_NONE - 匹配任何非 None 的值 # ANY_STRING - 匹配任意字符串(可附带前缀/包含条件) # assert_equal - 支持 ANY_* 的深度比较

各匹配器的语义如下(均来自源码实现):

  • ANY_BUT_NONEAnyButNone):__eq__对一切非None值返回True,用于“字段存在即可”的断言;
  • ANY_DICTAnyDict):匹配任意 dict,可通过containing({...})要求字典包含指定键值对;
  • ANY_LISTAnyList):匹配任意 list;
  • ANY_STRINGAnyString):匹配任意字符串,还支持链式starting_with("...")containing("...")实现部分匹配;
  • ANY:即unittest.mock.ANY(见 any_compare_helpers.py),用于精确类型之外的任意值。

断言助手

sdks/python/tests/testlib/assert_helpers.py 提供:

  • assert_equal(expected, actual):基于pytest_deepassert.equal的深度比较,支持ANY_*通配;
  • assert_dicts_equal(dict1, dict2, ignore_keys=None):比较两个 dict,可忽略指定键(E2E 验证器中大量使用,例如忽略动态生成的id);
  • assert_dict_has_keys(dic, keys):断言字典包含全部必需键;
  • assert_dict_keys_in_list(dic, keys):断言字典的所有键都在允许列表中;
  • assert_score_result(result, include_reason=True):针对ScoreResult的专用断言,校验scoring_failed is Falsevalue[0.0, 1.0]区间的 float,并可要求reason非空。

E2E 测试与 verifiers 验证器

当需要真正验证 SDK 与后端 API 的端到端行为时(即发起真实 API 调用、数据落到 ClickHouse),使用tests/e2e/verifiers.py提供的验证器。典型用法:

from tests.e2e import verifiers def test_trace_creation__e2e__happyflow(opik_client: opik.Opik): trace = opik_client.trace( name="test-trace", input={"query": "test"}, output={"result": "success"} ) verifiers.verify_trace( opik_client=opik_client, trace_id=trace.id, name="test-trace", input={"query": "test"}, output={"result": "success"}, )

验证器全家桶

verifiers.py 覆盖了 SDK 的几乎所有核心对象,每个验证器都接受opik_client、实体 ID 以及一组期望字段(未指定的字段默认mock.ANY,即不校验):

验证器验证对象关键能力
verify_traceTracename/input/output/metadata/source/tags/error_info/project_name/feedback_scores/guardrails_validations/comments
verify_spanSpan除 Trace 字段外,还校验trace_idparent_span_id、model/provider/total_cost
verify_datasetDataset描述与条目数量,忽略id键做逐条比较
verify_dashboardDashboardwidget 配置、布局与versionsection_count
verify_experimentExperiment名称、元数据、反馈分数量、trace 数、prompt 版本与实验级分数
verify_attachments附件大小、MIME 类型、下载链接前缀
verify_threadThreadid搜索线程并校验反馈分
verify_prompt_version/verify_chat_prompt_versionPrompt模板、类型、版本 ID、commit、环境归属

底层机制:轮询直到断言通过

E2E 数据在 ClickHouse 中是最终一致性(eventually-consistent):创建操作落地快,但后续 update(例如离线队列重放的消息)可能需要更长时间才被摄取。因此verify_trace/verify_span内部通过_retry_until_assertions_pass(见 verifiers.py)反复执行同一断言体:

  • 断言体即_check(),内部直接使用asserttestlib.assert_equal,比较逻辑只维护一份;
  • 轮询期间吞掉Exception(404 未摄取、瞬时网络抖动等),但pytest.fail.Exception继承自BaseException,需显式转为返回值,否则轮询会退化成单次尝试;
  • 轮询受synchronization.until(..., max_try_seconds=...)限制,超时后重跑一次check(),把真实异常与 traceback 抛给 pytest。

E2E 环境的自动化配置

sdks/python/tests/e2e/conftest.py 中的configure_e2e_tests_envfixture 会在每个测试模块期间通过testlib.patch_environ注入OPIK_PROJECT_NAME:测试文件声明PROJECT_NAME常量则使用之,否则生成e2e-<module>前缀的唯一项目名。同时opik_clientfixture(conftest.py)每测试构建新的Opik(batching=True)客户端,避免全局缓存客户端造成状态泄漏。

参数化测试:多场景驱动

对于同一断言逻辑、多组输入输出的场景,使用pytest.mark.parametrize

@pytest.mark.parametrize( "text,expected_sentiment", [ ("I love this product!", "positive"), ("This is terrible.", "negative"), ("The sky is blue.", "neutral"), ], ) def test_sentiment_classification(text, expected_sentiment): metric = Sentiment() result = metric.score(text) assert expected_sentiment in result.reason

参数化与命名规范搭配后,每个参数组合都会生成独立的测试节点,失败时可直接定位到具体输入。

四条核心规则

仓库对新增测试有明确要求(见 .agents/skills/python-sdk/testing.md):

  1. 只测试公共 API(public API only)——不要触碰私有实现细节,保证测试对重构有免疫力;
  2. 集成测试一律使用fake_backend——快速、无网络、无外部依赖;
  3. E2E 测试使用verifiers验证器——统一轮询与断言逻辑,避免每个用例自己重复实现“等数据落库”的样板代码;
  4. 动手前先研究已有相似测试——仓库tests/library_integration/(129 个用例)、tests/e2e/(65 个用例)是现成的模式库。

本地运行 E2E 测试

E2E 测试需要真实后端,CI 工作流会隐式设置若干环境变量,本地运行时需手动补齐。根据你的工作内容选择后端启动方式:

方案 A:CI 等价方式(推荐用于跑全量套件)

后端跑在 Docker 中,行为与 GitHub Actions 一致:

# 后端以 Docker 方式启动(与 GitHub Actions 匹配): TOGGLE_RUNNERS_ENABLED=true ./opik.sh --backend # 然后运行测试套件: cd sdks/python OPIK_URL_OVERRIDE=http://localhost:5173/api/ \ venv/bin/pytest tests/e2e/ \ --ignore=tests/e2e/test_guardrails.py \ -vv --durations=20

方案 B:dev-runner(迭代后端代码时使用)

原生 Java 后端会继承你的 shell 环境变量,因此启动前必须导出 MinIO 凭据与 runners 开关,否则附件与 runner 相关测试会因环境问题(而非真实回归)失败:

export AWS_ACCESS_KEY_ID=THAAIOSFODNN7EXAMPLE export AWS_SECRET_ACCESS_KEY=LESlrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY export TOGGLE_RUNNERS_ENABLED=true ./scripts/dev-runner.sh --restart cd sdks/python OPIK_URL_OVERRIDE=http://localhost:8080/ \ venv/bin/pytest tests/e2e/ \ --ignore=tests/e2e/test_guardrails.py \ -vv --durations=20

关于 AWS_* 凭据的说明

AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY是 MinIO 的 root 用户/密码(来源于 deployment/docker-compose/docker-compose.yaml,其中MINIO_ROOT_USERMINIO_ROOT_PASSWORD的默认值正是这两串EXAMPLE字符串,参见该文件第 114-115、226-227 行)。不涉及任何真实 AWS 账户——Java 后端的 S3 客户端在通过S3_URL指向 MinIO 时,复用了标准 AWS 环境变量名。

常见的坑(gotchas)

  • TOGGLE_RUNNERS_ENABLED:在 docker-compose 中默认为false。不开启的话,tests/e2e/runner/目录下 8 个测试会因后端未启用 runners 功能而在 setup 阶段直接报错;
  • --ignore=tests/e2e/test_guardrails.py:guardrails Python 服务不属于默认 compose 栈的一部分。CI 也显式忽略该文件(见 .github/workflows/python_sdk_e2e_tests.yml);guardrails 的 E2E 由独立工作流 guardrails_e2e_tests.yml 负责,其中同样设置了TOGGLE_RUNNERS_ENABLED: "true"并单独运行pytest tests/e2e/test_guardrails.py
  • MinIO 凭据:只在方案 B(dev-runner)中需要手动导出。方案 A 的 Docker 后端容器已内置这些凭据。

调试失败的 E2E 测试

当某个 E2E 用例失败时,按以下步骤分层排查:

# 把 SDK 的 DEBUG 日志捕获到文件: OPIK_FILE_LOGGING_LEVEL=DEBUG OPIK_LOGGING_FILE=/tmp/opik-sdk.log \ venv/bin/pytest tests/e2e/test_tracing.py::test_name -vv
  • OPIK_FILE_LOGGING_LEVEL=DEBUG:打开 SDK 的文件日志输出,追踪 SDK 侧的上报行为与重试逻辑;
  • OPIK_LOGGING_FILE=/tmp/opik-sdk.log:指定日志落盘位置,避免干扰终端输出。

后端侧的错误日志取决于启动方式:

docker logs opik-backend-1 # 方案 A:查看 Docker 后端容器日志 tail -f /tmp/opik-opik-backend.log # 方案 B:dev-runner 后端日志

一个实用的判别技巧:先判断失败是“环境问题”还是“真实回归”。若失败集中在 attachments、runner 相关用例,优先检查TOGGLE_RUNNERS_ENABLED是否开启、MinIO 凭据是否导出;若 SDK DEBUG 日志显示数据已成功上报而后端报错,再结合后端日志定位服务端问题。此外,E2E 验证器的轮询机制意味着偶发性失败往往与数据摄取延迟有关,可先用--count或重跑确认是否稳定复现,再深入分析。

【免费下载链接】comet-llmDebug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards.项目地址: https://gitcode.com/GitHub_Trending/co/comet-llm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询