码农高天
通过在Docstring中嵌入标签并结合Pytest收集钩子,开发团队无需重构用例即可建立Opt-in过滤机制,根治传统跳过装饰器易漏配挂死CI的问题。
AI-assisted summary of 用注释对测试进行分类?这给你能的! — original source. Check the original for full context.
Source published:
Key ideas
- 针对 GitHub CI 虚拟机缺乏 GPU 的硬件限制,码农高天提出了一种基于文档字符串标签的轻量测试分类方案,有效解决了异构环境下的用例调度问题。 (0:00)
- 传统基于目录分类或 unittest.skipIf 装饰器的方案因多维冲突、侵入性强以及容易漏标导致 CI 挂掉等痛点,在实际项目中难以落地。 (0:00)
- 通过在 conftest.py 中利用 Pytest 的 pytest_addoption 与 pytest_collection_modifyitems 两个 Hook 钩子,可在不依赖第三方插件的情况下原地过滤包含 #cpu 等标签的测试用例。 (3:40)
- 基于 Docstring 的 Opt-in 模式将代码修改成本降至最低,既不改变开发者的编写习惯,又天然保障了未标注测试在受限 CI 环境下的安全性。 (3:40)
Detailed notes
## Background 该视频由码农高天于 2024 年 12 月 13 日发布在 YouTube 平台。视频简介指出,在多环境与不同场合下对测试用例进行合理分类是常见需求,作者以此分享了自己在实际工作中遇到的硬件依赖冲突问题及最终落地的解决方案。
## TL;DR 面对 GitHub CI 仅有 CPU 资源而仓库中混杂 GPU 依赖测试的场景,传统的按目录分流、`skipIf` 装饰器或基类继承方案存在样板代码臃肿、高侵入性及 Filter-out 模式易挂 CI 等硬伤。高天提出了一种利用测试函数 Docstring 标注 `#cpu` 标签的轻量解法,兼顾了快速测试等多维度扩展。在框架底层,只需在 `conftest.py` 中利用 Pytest 的 `pytest_addoption` 与 `pytest_collection_modifyitems` 钩子函数,便能在不破坏开发者原有代码习惯的前提下,以极少代码实现安全优雅的 Opt-in 动态测试集过滤。
## Atmosphere & style 视频展现出务实而接地气的工程探讨氛围,风格明快利落且充满极客趣味。讲解从最直观解法的逐一碰壁展开,一步步将听众带入工程权衡的推导逻辑中,生动还原了一线开发者的思考脉络。整场分享像是一位资深技术专家在白板前与同事复盘架构设计,在四两拨千斤的“奇招”落地瞬间给人带来强烈的解题快感。
## Bulletin - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 针对 GitHub CI 虚拟机缺乏 GPU 的硬件限制,码农高天提出了一种基于文档字符串标签的轻量测试分类方案,有效解决了异构环境下的用例调度问题。 - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 传统基于目录分类或 unittest.skipIf 装饰器的方案因多维冲突、侵入性强以及容易漏标导致 CI 挂掉等痛点,在实际项目中难以落地。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 通过在 conftest.py 中利用 Pytest 的 pytest_addoption 与 pytest_collection_modifyitems 两个 Hook 钩子,可在不依赖第三方插件的情况下原地过滤包含 #cpu 等标签的测试用例。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 基于 Docstring 的 Opt-in 模式将代码修改成本降至最低,既不改变开发者的编写习惯,又天然保障了未标注测试在受限 CI 环境下的安全性。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 工程团队引入 GitHub CI 虚拟机后,由于环境仅有 CPU 而无 GPU,急需筛选并仅运行非 GPU 依赖的测试用例。 - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 按物理目录结构拆分测试并不现实,因为测试文件目录通常按业务功能划分,拆分会打破既有的模块组织且难以应对多维交错需求。 - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 使用 unittest.skipIf 等传统跳过装饰器存在样板代码膨胀的问题,且其逻辑是 Filter-out 而非 Opt-in,开发者漏标就会直接导致 CI 挂掉。 - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 测试套件往往存在硬件依赖与耗时维度(如几分钟的机器学习慢速测试与毫秒级快速测试)的正交分类需求,多重 skip 条件难以扩展。 - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 基于 unittest.TestCase 基类继承和 setUp 的方案具有强侵入性,会强制要求习惯编写函数式测试的开发者改造代码。 - [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 理想方案需具备 Opt-in 特性:默认不标注即不进受限 CI 运行,只有明确显式标注的用例才会被调度执行。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 最终方案采用在测试函数 Docstring 中添加类似社交媒体的标签(如 #cpu),对业务逻辑完全无副作用且书写零门槛。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) Docstring 属于 Python 原生元数据,即使标签书写错误也不会引发语法或运行时异常,提供了极高的容错性。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 在 conftest.py 中实现 pytest_addoption 钩子函数,可以方便地向 Pytest 注册 --cpu-only 等自定义命令行参数。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 通过 pytest_collection_modifyitems 钩子,Pytest 在收集完全部测试项后、执行测试前,允许原地遍历并过滤 items 列表。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 在钩子函数中直接检查测试函数的 __doc__ 是否包含指定 hashtag,未包含该标签的测试项会被安全移出执行队列。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 选用 hashtag 形式是因为常规 Docstring 几乎不会自然出现此类格式,兼具语义直观性与低冲突率,也可根据需要自定义唯一标识符。 - [03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 该钩子本质上是运行标准 Python 代码,未来可零成本扩展标签的交集、并集运算以及反向排除等更复杂的调度策略。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=0s) 业务背景与传统测试分类方案的痛点
码农高天在工作中遇到了一个测试分类的实际问题:代码仓库中的测试用例存在异构依赖,一部分测试必须依赖 GPU 才能执行,而另一部分测试只需要 CPU 即可运行。最近团队在 GitHub 上搭建了 CI 工作流,但 CI 提供的虚拟机只有 CPU 而没有配备 GPU,因此必须找到一种可靠的方法,在特定的 CI 环境下仅筛选并运行那些只需要 CPU 的测试。
面对这种需求,团队首先梳理了几种常见的解决思路,但都在实际工程落地中遇到了阻碍: - **按文件目录结构分类**:最直观的方式是将 GPU 测试与 CPU 测试分别存放于不同的文件或目录中,在 CI 流程中只执行对应的文件。然而在绝大多数成熟项目中,测试文件的目录层级已经用来承载业务功能模块的划分,同一个业务功能的测试集往往同时包含 CPU 和 GPU 用例。如果硬要利用物理文件路径做多维度的正交分类,不仅非常繁琐,而且得不偿失。 - **使用 `unittest.skipIf` 或跳过装饰器**:另一种方案是在用例上添加环境变量判断条件,环境不满足就自动跳过。但高天指出这种方式存在三大硬伤: 1. **样板代码膨胀**:每个测试函数都需要增加额外的检查代码。 2. **逻辑为 Filter-out 而非 Opt-in**:默认行为是全部执行,如果新加入团队的开发者写了一个需要 GPU 的测试却漏加了跳过标记,CI 就会直接报错挂掉。正如高天所强调的:“而我们更想要的是一个和它相反的逻辑,也就是所谓的 ‘opt in’,就你正常写你的测试无所谓,如果你的测试什么都没做我就认为它不能在CPU上跑,只有你这个测试做了一点事儿我才会在CI上跑这个测试。” 3. **可扩展性极差**:除了 CPU 与 GPU 的硬件维度,团队还有执行耗时的维度需求。许多与机器学习(Machine Learning)相关的测试执行缓慢,动辄需要几分钟,团队需要能够将一些运行飞快的用例标注为 Quick Test,以便开发者在本地边写边快速验证,开发完成后再跑完整慢速测试。若继续叠加多层 `skip` 条件,整体逻辑将变得极其复杂脆弱。 - **基于类结构的 `setUp` 与基类继承**:虽然可以通过把测试归入不同的 `TestCase` 类,利用 `setUp` 或 `setUpClass` 统一检测硬件,甚至编写一个通用的 Base Class,在基类的生命周期里实现 Opt-in 逻辑来减少重复代码,但这依然带来了强侵入性。它不仅要求所有测试都必须强制重构成面向对象的 `unittest.TestCase` 结构,而团队里部分成员习惯写独立的函数式脚本测试,这会为不熟悉该框架的开发者增加额外的改造成本。
```mermaid flowchart TD accTitle: 测试用例运行模式对比 accDescr: 对比传统 Filter Out 模式与期望的 Opt In 模式在处理新增测试时的逻辑差异。
subgraph FilterOut["传统 Filter-out 模式 (易挂 CI)"] A1["开发者新增 GPU 测试"] --> B1{"是否手动加了 skip 标记?"} B1 -- 否 --> C1["CI (仅 CPU) 默认执行该测试 --> CI 报错中断"] B1 -- 是 --> D1["成功跳过"] end
subgraph OptIn["理想 Opt-in 模式 (安全稳健)"] A2["开发者新增测试"] --> B2{"是否主动标注 #cpu?"} B2 -- 否 --> C2["CI (仅 CPU) 默认忽略 --> 安全通过"] B2 -- 是 --> D2["CI (仅 CPU) 纳入测试执行列表"] end ```
---
[03:40](https://www.youtube.com/watch?v=uPd8z0hT6qY&t=220s) 基于 Docstring 标签与 Pytest Hook 的轻量解法
在梳理完所有限制后,高天提炼出该方案的核心诉求:第一,维持现有的开发习惯,不能对开发者的日常编写流程产生侵入式改变;第二,提供一种极简、可选(Opt-in)的标注机制,标注了可在 CPU 跑的就进 CI,标注了 Quick 的就进快速测试套件。最终,他设计的交互接口非常轻巧:开发者只需在测试函数的 Docstring(文档字符串)中加入类似社交媒体的标签 `#cpu`。
```python def test_something(): """这是一个测试函数 #cpu""" assert True ```
由于 Python 的 Docstring 仅作为元数据存在,不参与任何底层执行逻辑,开发者可以极具安全感地在其中打上各种标签,即便写错也不会影响代码本身的语法与运行。而在 CI 或命令行中,只需通过 Pytest 传入自定义参数 `--cpu-only` 即可精确筛选。
高天随后详细拆解了该机制背后的底层实现方式。开发者无需引入复杂的第三方插件,只需在测试目录根路径或项目根目录下新建一个 `conftest.py` 文件,利用 Pytest 官方提供的两个核心 Hook 钩子函数即可完成全部逻辑: 1. **注册命令行参数**:通过实现 `pytest_addoption` 函数,向 Pytest 的命令行解析器中注册 `--cpu-only` 标志位。 2. **过滤收集到的用例**:通过实现 `pytest_collection_modifyitems(config, items)` 钩子函数。当 Pytest 完成测试目录扫描并收集到所有可用测试项(封装在 `items` 列表中)后,Pytest 会在实际运行前调用该钩子。此时,代码检查 `config` 中是否启用了 `--cpu-only` 选项;若未启用,则保持原样全量运行;若已启用,则遍历 `items` 列表,读取每个测试项对应函数对象的 Docstring(文档字符串),检查字符串内是否包含 `#cpu`。凡是不包含该标签的测试项全部被过滤丢弃,最后将筛选后的列表替换回原来的 `items`。
高天提到,利用这个钩子函数的本质就是在执行普通的 Python 代码,因此不仅能实现包含检查,还可以轻松扩展多标签交并集、选择 Filter-in 或 Filter-out 等任意复杂的定制策略。关于直接匹配 `#cpu` 是否会误伤正常文档内容的问题,高天解释道:“但是对于我们来说,docstring 里面是几乎不可能出现这种 hashtag 的形式的,这也是选择 hashtag 的一个原因,但是另一个原因是 hashtag 这东西现在本来就大量的被当做标签使用。”如果特定项目确实有特殊格式顾虑,也可以将标签设计得更具唯一性。最终,整套方案在框架侧仅需几行干净的 Hook 代码,在开发者侧只需写一个简单的 `#tag` 注释,既最大化减轻了维护负担,又优雅满足了异构硬件下的测试调度需求。
- **核心涉及的 Pytest Hook 函数**: - `pytest_addoption`:用于向命令行注入自定义参数选项(例如 `--cpu-only`)。 - `pytest_collection_modifyitems`:在用例收集完毕、执行开始前提供原地修改、过滤或重新排序用例列表 `items` 的能力。 - **Docstring 方案的工程收益**:零依赖侵入、完全保持原有函数式或面向对象写法、天然支持 Opt-in 渐进式标注、排查与代码可读性极高。
## Mentioned 码农高天, GitHub, CI, GPU, CPU, unittest, pytest, pytest_addoption, pytest_collection_modifyitems
## Video description 有的时候我们需要对我们的测试进行分类,然后在不同的环境和场合下运行不同的测试集。这次我和大家分享一下我在工作中遇到的一个真实问题,以及我最后给出的解决方案。
[Source](https://www.youtube.com/watch?v=uPd8z0hT6qY)