Opik 端到端测试标签体系:用 Playwright 标签构建可验证的覆盖率地图
【免费下载链接】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
导读
本文以 tests_end_to_end/TESTING-TAGS.md 为骨架,系统讲解 Opik 仓库端到端测试(Playwright 功能测试 + 视觉回归测试)的标签语法(tag grammar)、分层执行策略与覆盖率治理机制。读完本文,你将掌握@t1-smoke、@t2-cuj、@t3-nightly、@area:、@cap:、@vcap:六类标签各自的语义与组合规则,理解标签如何被 CI 中的tag_lint.py强制校验、如何被 Allure 报表消费,以及如何为新增测试区域与能力制定符合规范的分层方案。
一、标签是覆盖率地图,而不是装饰
在 Opik 的端到端测试体系中,标签承担着一个核心职责:它是测试覆盖率的唯一事实来源。正如文档开篇所强调的:
没有人维护一张"哪些测试覆盖了 prompts"的电子表格——spec 本身就是地图,覆盖率构建器(coverage builder)直接读取这些标签。
这句话有两层含义:
- 声明即覆盖:一条能力(capability)被标记为"已覆盖",依据是存在一条携带其
@cap:标签的测试,而非人工填写的台账。 - 语法必须成立(grammar holds):既然覆盖率完全依赖标签的机器可读性,标签语法就必须被 CI 强制执行,否则地图就会失真。
这套机制的**作用域(Scope)**被严格限定为两个目录:
tests_end_to_end/e2e(Playwright 功能测试,functional)tests_end_to_end/visual-tests(Playwright 视觉回归测试,visual)
除此之外的任何测试(例如仓库中 tests_load 目录下的压测脚本)都不在这套标签体系内,这在后面"覆盖率维度"一节会再展开。
二、四类标签:tier / suite / area / cap
标签只允许出现在 Playwright 的tag选项里,绝不能写进测试标题。一个典型的标签声明如下:
test.describe('Prompt Library — smoke', { tag: ['@t1-smoke', '@area:prompts', '@cap:prompts.list-prompts'], }, () => { /* ... */ });四类标签按"回答什么问题"划分:
| 类别(Kind) | 基数(Cardinality) | 示例 | 回答的问题 |
|---|---|---|---|
| tier | 恰好 1 个 | @t1-smoke | 跑多深 / 多久跑一次? |
| suite | 0 或多个 | @t1-stsaas | 除了分层阶梯,还要在哪里跑? |
| area | 恰好 1 个 | @area:prompts | 属于哪个产品区域? |
| cap | 1 个或多个 | @cap:prompts.list-prompts | 覆盖了哪些能力? |
这种设计把"跑得多深"和"在哪里跑"这两个正交维度拆开,而不是用一个扁平枚举混在一起——这是理解整套体系的钥匙。
2.1 tier——深度与频率
| 标签 | 运行时机 | 成本要求 |
|---|---|---|
@t1-smoke | 合并后(post-merge)+ 生产环境每天 3 次 | 必须廉价且稳定 |
@t2-cuj | 每夜(作为 t3 的一部分) | 真实用户旅程(Critical User Journey) |
@t3-nightly | 每夜在 staging 上 | 最慢、最彻底 |
tier 是累积的:test:t2会跑 t1+t2,test:t3会跑 t1+t2+t3。这一规则在 tests_end_to_end/e2e/package.json 的 npm scripts 中得到了精确实现:
"test:t1": "playwright test --grep @t1-smoke", "test:t2": "playwright test --grep \"@t1-smoke|@t2-cuj\"", "test:t3": "playwright test --grep \"@t1-smoke|@t2-cuj|@t3-nightly\"",因此选择 tier 的依据是**"它应该多频繁地运行"**,而不是"它有多重要"。一个重要但昂贵的测试应当属于 t3 而非 t1——把它放进 t1 只会拖慢高频回归并制造不稳定。
2.2 suite——决定"在哪里",而不是"有多深"
文档特别强调:suite 与 tier 正交,这是最容易搞错的部分。一个 suite 标签表示"也把我包含进这次运行",与测试深度无关。一条 spec 可以同时是@t2-cuj和@t1-stsaas——拥有 t2 的深度,同时被纳入 STSaaS 的客户环境 sanity 集合。
| 标签 | 含义 |
|---|---|
@t1-stsaas | 纳入 STSaaS 客户环境 sanity 运行(test:t1-stsaas) |
@provider-sanity | LLM 提供商矩阵;自有节奏,不阻塞部署 |
文档给出的实战案例是 optimization-studio/optimization-studio.spec.ts:
test.describe('Optimization Studio — core', { tag: ['@t2-cuj', '@t1-stsaas', '@area:optimization-studio'] }, () => { test('the new-run form renders its sections and enables Optimize only once valid', { tag: ['@cap:optimization-studio.new-run-form-validation'] }, async ({ /* ... */ }) => { // ... }); });选择@t2-cuj是因为它是一条完整的用户旅程;选择@t1-stsaas是因为 Optimization Studio 在客户环境上历史上不稳定,必须在那里验证;不选@t1-smoke,是因为每次运行都会消耗真实的 LLM 预算,而 t1 每天跑 3 次。
同样合法的情况是:一条 spec只携带 suite 标签而没有 tier——这是刻意退出分层阶梯(opt-out)。例如 playground/playground-providers.spec.ts 就只带@provider-sanity,它按自己的节奏运行,不参与 t1/t2/t3 阶梯。
2.3 area 与 cap——覆盖率本体
@area:表示该 spec断言的能力所属的产品区域。关键约束:区域名要从coverage/taxonomy.yaml读取,而不是从 spec 所在的目录名推断(详见"五、spec 放哪里")。@cap:的格式必须是<area>.<capability>,且两半都必须在 taxonomy 文件中存在。目前只有@cap:与@area:的匹配关系由tag_lint.py强制检查。
一个来自 datasets/dataset-items.spec.ts 的完整例子:
test.describe('Dataset items — direct coverage', { tag: ['@area:datasets'] }, () => { test('Editing an item field commits as a new version and round-trips to the SDK', { tag: ['@t2-cuj', '@cap:datasets.edit-item-versions'], }, async ({ dataset, project, backendClient, page }) => { // ... }); });为 spec 实际断言的每一条能力添加@cap:,而不仅仅是最显眼的那一条;同时只为自己真正断言的能力添加。覆盖率的口径是:只要存在携带该标签的测试,能力即视为已覆盖;测试的健康状况(绿/不稳定/红)被单独跟踪和报告。这带来两个方向上的纪律:
- 未列出(unlisted)的断言是不可见的覆盖:如果测试断言了某个行为但没有对应
@cap:,覆盖率地图对此完全盲区; - 列了却没断言(listed but not asserted)则是永久的假绿(permanent false green):标签声明了覆盖,实际没有任何测试在守护它。
2.4 Visual specs——独立的@vcap:体系
视觉测试携带@vcap:且不带 tier——它们作为一个整体套件运行。参考 visual-tests/tests/empty-states.spec.ts:
{ tag: ['@vcap:datasets.datasets-empty'] }视觉能力是**页面/状态形态(page/state-shaped)**的,而非行为形态:一张截图断言整个页面渲染正确。它们位于每个区域的visual:块中,且每个都必须声明一个state:,取值限定为default | empty | loading | error四种枚举值。
在 taxonomy.yaml 中可以看到视觉能力的实际组织方式,例如 traces 区域下的:
visual: logs-traces-view: { covered: true, state: default, spec: "visual-comparison.spec.ts 02" } logs-traces-empty: { covered: true, state: empty, spec: "empty-states.spec.ts E01" } trace-sidebar-messages: { covered: true, state: default, spec: "trace-sidebar.spec.ts S01" }三、标签放哪里:describe 与 test
把共享标签放在describe上,单测特有标签放在test上。覆盖率构建器会做并集操作(unions),因此每个 test 自动继承其外层 describe 的全部标签。
test.describe('Dataset items', { tag: ['@area:datasets'] }, () => { test('editing commits a version', { tag: ['@t2-cuj', '@cap:datasets.edit-item-versions'], }, async () => { /* ... */ }); test('bulk delete commits a version', { tag: ['@t3-nightly', '@cap:datasets.bulk-delete-items'], }, async () => { /* ... */ }); });一个文件可以包含多个处于不同 tier 的 describe——ollie/ollie-agentic.spec.ts 就有三个,这完全合法且往往是正确的组织方式。代价是 tier 的基数(cardinality)检查因此无法在文件级做,具体见"八、CI 强制"。
四、命名规范
标签:kebab-case 小写。@cap:为<area>.<capability>,两侧都保持 kebab-case。
测试标题:<capability>: <behavior>的语义——描述"什么必须为真",而不是"点了什么按钮"。文档给出了正反例:
// good —— 说明必须成立的不变式 test('Editing an item field commits as a new version and round-trips to the SDK') // bad —— 描述的是点击动作,而非保证 test('click edit then save then check')这一规范在 dataset-items.spec.ts 中落地得相当彻底:"Editing an item field commits as a new version and round-trips to the SDK"、"Bulk-deleting selected items commits as a new version and round-trips to the SDK"、"Searching filters the items table to matching rows"——每条标题都在陈述产品承诺,而不是 UI 操作序列。
五、spec 放哪里:区域不来自目录
@area:不能从目录名推导。必须读取 taxonomy 中该区域的spec_dir字段——目录以产品表面命名,而一个目录可以承载多个区域。当前仓库就有两个真实案例:
| 目录 | 承载的区域 | 原因 |
|---|---|---|
trace-explore/ | @area:traces、@area:threads | 两个区域都声明spec_dir: trace-explore;UI 把这块表面称为 "Logs" |
prompts/ | @area:prompts、@area:playground | 四条 prompt→playground 旅程 spec 按所演练的流程而非区域分组 |
这一点在 taxonomy.yaml 中可以看到直接的代码级证据:traces区域声明spec_dir: trace-explore,threads区域也声明spec_dir: trace-explore;prompts区域的specs:列表里则出现了prompts/prompt-playground-new-prompt.spec.ts等"跨区域"文件(这些 spec 的@cap:前缀是 playground,但文件躺在 prompts 目录下)。
所以:同一个目录中出现两个不同的@area:值是常态,不是错误;跨区域的旅程 spec 放在它所演练的流程的邻居之间。新增 spec 时的操作顺序是:
- 把文件放在其流程邻居所在处;
- 用"它断言的区域"打
@area:标签; - 把它加入该区域的
specs:列表(保持排序插入,见第八节)。
六、test.step():把失败定位到阶段
用test.step()包裹每个阶段。失败时 Allure 会以失败步骤命名报告,从而把"测试坏了"细化为"播种成功,但对 version 2 的断言失败了":
const dataset = await test.step('Seed a dataset via the SDK', async () => { /* ... */ }); await test.step('Edit an item field and commit', async () => { /* ... */ }); await test.step('Verify the edit round-trips to the SDK', async () => { /* ... */ });这是仓库既有的事实风格:24 条功能 spec 中有 22 条、21 个 page object 中有 16 个在使用它。test.step()支持返回值和模板字符串标题——dataset-items.spec.ts 中const items = await test.step('Open the dataset items page', async () => { ... })正是"步骤返回值"的用法,返回值被后续步骤复用。
七、Allure 集成:标签自动到达报表
Playwright 标签会自动流入 Allure,无需任何allure.label()调用。Allure 会去掉开头的@,因此@cap:prompts.list-prompts可以按如下方式查询:
tag = "cap:prompts.list-prompts"复合查询同样可用,这正是覆盖率构建器所依赖的能力:
tag = "area:traces" and status = "passed"这一机制在 tests_end_to_end/e2e/playwright.config.ts 中由allure-playwrightreporter 配置支撑:
reporter: [ ['line'], ['html', { outputFolder: 'playwright-report', open: 'never' }], ['json', { outputFile: 'test-results/results.json' }], ['allure-playwright', { outputFolder: process.env.ALLURE_RESULTS || 'allure-results', detail: true, suiteTitle: true, }], ],所有结果上报到project 1(Opik 与 EM 共用),因此需要按 launch name 或标签分段查看,绝不能按 project id 区分。
八、CI 强制:tag_lint.py 与 GitHub Actions
.github/workflows/tag_lint.yml在每次触碰tests_end_to_end/的 PR 上运行(硬失败,而非警告)。硬失败的原因很现实:一条未打标签的 spec 对--grep不可见,会静默地永远不运行——而 package.json 中的test脚本使用了--pass-with-no-tests,即使零测试被匹配也会"通过"。这就是这套 lint 要防住的失败模式。
8.1 本地复现 CI 校验
与 CI 完全一致的方式在仓库根目录运行:
pip install pyyaml python3 tests_end_to_end/coverage/tag_lint.py \ --taxonomy tests_end_to_end/coverage/taxonomy.yaml \ --estate tests_end_to_endlint 产出的 findings 是**行锚定(line-anchored)**的,因此在 CI 中使用--format github时,问题会直接出现在 PR 的 Files-changed 视图中对应代码行上。
8.2 强制项清单
- 每条非豁免的 e2e spec 必须有 tier(或 suite 豁免)且恰好一个
@area:; - 每条非视觉 spec 至少声明一个
@cap:; @area:/@cap:/@vcap:都必须在 taxonomy 中可解析;@cap:必须位于该 spec 声明的 area 之下;- 视觉 spec 携带
@vcap:且不带 tier; - taxonomy 中每个视觉能力都必须有枚举内的
state:; - 不允许出现无法识别的标签。
8.3 明确不强制的事项
tier 基数(cardinality)不在强制范围内。"恰好一个 tier"约束的是单条测试——即 describe 继承之后的状态;而 linter 只读取字符串字面量,不解析 TS AST,它无法区分"同一文件里多个 describe 的 tier 各不相同(合法,有 4 条 spec 这么做)"与"一条测试携带两个 tier(错误)"。这一条要靠人来维持正确。
8.4 计算型标签的陷阱
计算得到的标签是不可见的:tag: [variant.cap]能通过 lint,但对覆盖率毫无贡献——因为 lint 和覆盖率构建器都只匹配tag: [...]内的引号字符串字面量。因此,如果在循环里生成测试且每次迭代覆盖不同能力,必须改写为带字面量标签的独立test()调用——prompt-library-smoke.spec.ts 就是正确做法的范例。
8.5 豁免目录与旧标签迁移
豁免(rules.exempt_dirs)只有_seed(harness 自测)。遗留裸区域标签(如旧式的@datasets)和退役标签会得到明确指出替换方案的报错信息,而不是一句笼统的 "unrecognised"。
九、taxonomy.yaml:定义"100%"的评审文件
coverage/taxonomy.yaml 是整套覆盖率的根基:这个文件定义了"100%"。每个区域的覆盖率 = 拥有至少 1 条近期通过测试的能力数 ÷ 该区域总能力数。因此:
向 taxonomy 中添加能力会改变分母并降低覆盖率,直到测试落地——这是刻意设计:让已知缺口显性化,而不是静默缺席。
9.1 新增区域或能力的三步流程
- 在
coverage/taxonomy.yaml中添加区域或能力(标记covered: false); - 提交评审——这是 QA 拥有的决策,不是实现细节;
- 随着覆盖落地,用新
@cap:给 spec 打标签并翻转covered: true。
9.2 能力粒度的把握
能力的"海拔"建议为每区域 5–15 条,每条都是测试可合理断言的用户可见行为:
- ✅ "Create a prompt" 是一条能力;
- ❌ "Click the save button" 不是;
- Tab 通常应该是独立能力。
9.3 区域重命名
重命名区域时,把旧名字加入tag_aliases:,保证历史 Allure 结果仍可解析。如果旧标签已无法映射到唯一区域,则加入retired_tags:并按 spec 逐一解析——trace-explore拆分进traces与threads就是这一场景,taxonomy 中记录了完整的拆分理由:
retired_tags: trace-explore: reason: "split into `traces` and `threads` — resolve per spec, not by rename" replaced_by: [traces, threads]此外,taxonomy 还记录了现实世界中另一个别名场景:annotation-queues区域带tag_aliases: [annotation-queue]——这是历史上单复数不一致的遗留,lint 在报错时会提示 canonical 名称。
9.4 三个覆盖率维度
taxonomy 将覆盖率拆成三个独立维度,绝不混合成一个数字:
- functional(
@cap:):源自tests_end_to_end/e2e,适用性为all——每条能力都应正常工作; - visual(
@vcap:):源自tests_end_to_end/visual-tests,适用性为opt_in——只有声明了visual: {...}的能力才计入分母,避免把"空态截图"虚增成功能覆盖; - load(
@lcap:):源自仓库外的tests_load,当前状态为planned——压测报告到 JUnit XML 而非 Allure,因此对 Allure 构建器不可见,v1 构建器必须跳过它。
applicability是防止 load 列数据失真的关键:大部分 UI 能力不承担负载,把它们算作"负载覆盖 0%"会凭空捏造一个无人打算填补的 180 条缺口。同样,taxonomy 中的cloud_only: true能力(如diagnostics、ollie)依赖仓库外的 comet 前端插件,被排除出自托管覆盖率视图,但计入云版本视图。
十、从源码验证:lint 的实现细节
tag_lint.py 是这套语法的执行者,其实现印证了文档中的全部声明:
- 正则而非 TS AST(第 27–30 行注释):被 lint 的标签永远是
tag: [...]数组中的字符串字面量,可可靠 grep;测试标题不在此列(3 条 spec 传变量、5 条用模板字符串),覆盖率构建器改用playwright test --list解析运行时标题。 - TAG_BLOCK 与 TAG_LITERAL 两个正则分别匹配
tag:\s*\[(.*?)\]和其中的引号包裹标签,支持单行与多行数组。 - 视觉 spec 分支:无
@vcap:报错;携带 tier 则报"visual spec must not carry a tier tag"。 - suite 豁免:
if not tiers and not suites_present时才算缺 tier——证实了"只带 suite 标签合法"的规则。 - 区域归属检查:
@cap:的.前缀必须等于声明的@area:,否则报'@cap:...' does not belong to declared area '@area:...'。 - taxonomy 自身也受检:每个
visual:能力必须有枚举内state:,且每个区域的specs:列表必须保持排序——这是为了并发场景:追加会把所有并发新增压在同一行导致 git 冲突,排序插入让它们落在不同行(taxonomy 头部注释记载该文件一度是 QA 队列中冲突最频繁的文件)。 - 退出码非零即失败,且只读("Read-only; never edits specs")。
结语:让标签成为一种工程纪律
Opik 的这套标签体系把三件事绑定在了一起:声明(tag)、执行(grep 选择)、度量(Allure 查询与覆盖率构建)。tier 决定频率、suite 决定场所、area/cap 决定覆盖声明、vcap 决定视觉断言——而 CI 的硬校验保证了语法不会腐烂。对新加入的 spec 作者来说,核心心法是三句话:按"该多频繁运行"选 tier;suite 与 tier 正交;为每条真实断言都声明@cap:,且只为真实断言声明。
【免费下载链接】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),仅供参考