本节摘要:Airflow 测试要同时覆盖“图对不对”和“函数在给定窗口下产不产出对的东西”。社区把测试命令与 DAG 即代码的边界逐步写进实践:结构测试断言依赖与默认参数,任务测试喂上下文,导入测试在干净环境 import,发布测试保证解析侧与执行侧同一版本。CI 不是多一个绿勾,是三层验证缺一不可。
阅读完本节,你应当能够:
原文把测试与 CI 拧成双螺旋:测试定义何为对,CI 保证一直对。三层是:语义(图是否表达意图)、运行时(给定输入是否得到输出与正确分支)、契约(何时何地允许上线)。缺语义,CI 只是更快地运送错误依赖。缺运行时,环境差异(SQLite 对 PostgreSQL、缺 Provider)会在生产才爆炸。缺契约,测试通过的版本仍可能被手动贴进生产目录。
结构测试示例概念:加载 DAG 对象,断言 task_id 集合、pull >> validate、catchup is False、retries 不超过团队上限。这跑得很快,每次提交都该跑。不必起调度器。
def test_min_dag_shape(dag): assert dag.catchup is False assert "validate" in dag.task_ids down = dag.get_task("validate").downstream_task_ids assert "load" in down
任务函数单测:把 python_callable 写成接受业务参数的函数,测窗口边界、空输入、上游失败是否不写库。不要在单测里真的连生产。外部用假 Hook。
导入测试:在与 Worker 相同的依赖环境里 import DAG 模块,收集 import_errors。这能抓住缺 Provider。官方有列出导入错误与列出 DAG 的运维命令,CI 里应等价地做一次加载。
顺序建议:代码规范扫描(密钥、顶层 Variable 模式)→ 结构测试 → 单测 → 构建执行镜像 → 用该镜像再跑导入测试 → 制品打版本 → 同步到调度器目录或镜像仓库 → DAG 保持暂停直到人工或自动冒烟 → 取消暂停。
生产取消暂停应是独立步骤,不要提交即全跑。金融类还要变更窗口。回滚是同步旧版本制品,不是在 UI 里改任务。
| 层 | 测什么 | 失败意味着 |
|---|---|---|
| 规范扫描 | 密钥、禁止 API | 不可合并 |
| 结构测试 | 边、参数、id | 图与意图不符 |
| 函数单测 | 窗口与幂等 | 业务逻辑错 |
| 导入测试 | 依赖完整性 | 镜像或包缺失 |
| 冒烟 | 一次手动窗口 | 连接或权限错 |
环境:CI 用真实 PostgreSQL 的最小库测导入,不要只用 SQLite,连接池行为不同。密钥用假连接。不要在 CI 打真实生产库。
动态映射测空列表与短列表。动态工厂测 id 唯一、数量上限。这些是结构测试的延伸,数据用夹具,不要读线上租户表。

够不是 100% 行覆盖。最低:每条生产 DAG 有结构测试;每个有分支或触发规则的图有状态组合测试;每个写库函数有幂等测试。传感器可用假就绪。不必在 CI 跑真正的小时级作业。
airflow tasks test 一类命令适合开发者本地按窗口跑单任务,不写完整 Run 历史或按版本有所差异。它不能替代结构测试,但能在接好连接的开发库上验证 SQL。不要用它点生产。
⚠️ 常见坑:CI 只跑 linter,DAG 依赖写反仍合并。第二天补数按错误边写库。
💡 关键直觉:把 catchup、retries、边当成断言对象,它们是契约,不是风格。
发布说明包含:窗口是否变化、是否需要 backfill、池是否变化。监控在发布窗口对导入错误告警做预期。回滚演练一年至少一次,否则回滚只是文档。
与平台团队接口:DAG 仓库与镜像仓库的版本号互相写入,出事故时能回答“当时 Worker 跑的是哪一版函数”。说不清版本的集群,测试再绿也无法复现。
冒烟用固定逻辑日期,数据是夹具或预发库,不断生产。检查:导入无错误、任务按边走、失败回调测试通道能响、日志可打开、XCom 只有路径。通过后取消暂停。回滚:同步上一版本制品,保持暂停,确认解析为旧图,再取消暂停。不要在 UI 里手工改任务参数当回滚,那会让 Git 与现场永久分叉。
夹具:窗口边界(月末、时区切换日若你们会碰到)、空分区列表、上游失败、重试后成功。不必覆盖所有 Operator 参数组合。覆盖契约:catchup 假、边对、幂等、空映射。TriggerDagRun 的测试用假目标或上限夹具,防止测试环境递归触发。
CI 失败分类:规范扫描红是秘密或禁止 API,必须修。结构测试红是图与意图不符。导入红是镜像。三类不要互相重跑碰运气。开发者本地应能跑结构和单测,导入测试可以只在 CI 镜像里跑,以保持笔记本轻量,但合并以 CI 为准。
原文把语义、运行时、契约并列为不可拆。契约层落地就是:生产目录只接受制品,制品只来自绿的流水线,流水线必须含结构与导入。人工把文件拷进生产目录,视为事故,哪怕文件内容碰巧对。碰巧对的路径无法复现,也无法回滚。
一般不必。结构与 import 足够挡大多数图错误。完整调度循环留给预发冒烟。CI 起全套会慢、会脆、会和单元测试抢时间。脆的 CI 会被人跳过,比没有更糟。把贵的测试放在预发,把便宜的测试放在每次提交。
tasks test 能代替 DagRun 吗?不能。它方便验证单任务在某窗口的函数行为,不验证依赖推进、不验证 max_active_runs、不验证回调是否在真实状态机里触发。当开发辅助,不当发布证据。发布证据是结构测试加导入测试加预发一次真 Run。
先补关键 DAG 的结构断言,再补写库函数的幂等测试,再补导入测试进 CI,最后才谈预发全链路。顺序反了会先建昂贵而脆的端到端,日常被跳过。债列表公开:哪条 DAG 还没有 catchup 断言。没有断言的 DAG 视为未完工,即使它在跑。未完工可以暂时服务业务,但不能当范本复制。复制未完工是测试债的利息。原文把可测试性写成成熟度门槛,门槛的最低一级就是结构测试覆盖关键图。覆盖率数字可以稍后,断言对象必须先是契约字段。字段对了,行覆盖自然会跟着函数单测长出来。
同步后默认暂停,可以由 CI 保证:制品带 paused 标记或发布步骤调用暂停。取消暂停是独立权限,对应 5.2 的放行权。自动化冒烟通过后可以自动取消非关键 DAG 的暂停,关键 DAG 仍人工。人工不是落后,是合同级放行。自动化失败应保持暂停并告警,不要“大部分成功就放行”。大部分成功里藏着那条关键日报。闸门的价值就是讨厌这种大部分。讨厌要写进脚本,不写进口号。脚本在夜深人静时仍然讨厌。口号在那时没有人。没有人的时候,只有 CI 还在决定生产目录长什么样。所以 6.3 把讨厌交给 CI。人负责写断言和抽查。人与 CI 的分工,就是测试与发布的全部哲学。哲学若要一句话:绿必须指同一份制品、同一份镜像、同一份暂停态。三同一,回滚才是反向同步,而不是考古。考古不是发布策略。
发布前核:制品哈希、镜像标签、暂停态,三者与流水线输出一致。不一致不准取消暂停。不准是闸门的最后一牙。牙断了,前面所有测试都可能测的是另一份代码。另一份代码绿,生产红,会摧毁对 CI 的信任。信任一摧毁,人会跳过 CI。跳过 CI,6.1 的三条断言作废。作废后 catchup 会回来。回来会抽干库。库是 3.1。3.1 依赖 6.3 的信任。信任依赖三同一。三同一依赖检查单。检查单可以自动化比对哈希。自动化比对比人眼可靠。人眼在夜发布会累。累会看错标签。看错标签会放行错误镜像。错误镜像会缺 Provider。缺 Provider 会导入失败。导入失败会让图消失。图消失是全局事故。全局事故比多等一分钟比对哈希贵。贵贱最后一次算清。算清就会自动化三同一。自动化在发布步骤。步骤失败则保持暂停。保持暂停是安全默认。默认比口号强。口号是“我们有 CI”。CI 测错对象时,口号是谎。谎用三同一拆穿。拆穿靠哈希。哈希靠检查单。检查单在本节。本节是教程最后一节。最后一节把讨厌交给脚本。脚本在无人时仍讨厌错误对象。讨厌错误对象,是测试哲学的最后一句。句完,DAG 才能交得出手。交得出手的意思是:能写、能懂谁执行、能活过故障、能拒绝越权、能挡住坏图、能证明测的是同一份。同一份靠三同一。三同一结束 6.3。6.3 结束第 6 章。第 6 章结束教程。教程结束于闸门。闸门结束于哈希。哈希结束于你愿意把讨厌写进脚本。写进脚本就结束。结束不是完美。完美不存在。存在的是可回滚的同一份。同一份在检查单。检查单要做。做了才能 done。done 在暂停取消之后。取消在比对之后。比对在本节。本节最后一次强调:绿必须指同一份制品、同一份镜像、同一份暂停态。三同一。同一结束。
关键 DAG 即使冒烟自动绿,仍要人眼看一眼三同一输出。一眼很便宜。便宜挡住标签看错。看错放行错误镜像。错误镜像缺包。缺包导入失败。失败图消失。消失全局事故。事故比一眼贵。贵贱最后确认。确认结束夜发布的累。累结束人眼。人眼结束于关键仍人工。人工结束落后的错觉。错觉结束合同级放行。放行结束大部分成功。大部分结束藏关键。关键结束闸门价值。价值结束讨厌。讨厌交给脚本,关键仍给人眼。人眼加脚本。加结束哲学。哲学结束绿必须同一份。同一份结束 6.3。6.3 结束第 6 章。第 6 章结束教程动手链。链结束于检查单被读。被读结束考古。考古结束。回滚成为反向同步。同步结束。交得出手结束。结束。
到这里,一条 DAG 从能跑、能懂、能活过故障、能被拒绝、能被测试,才算交得出手。