本节摘要:测试是数据工程的验收工序:字段非空、主键唯一、枚举合法、金额不为负,这些检查不该靠「跑完看两眼」,而应该写成断言,每次构建自动执行。本节讲 dbt 测试体系的两个层次——覆盖结构性校验的通用测试与覆盖业务规则的自定义测试,严重级如何决定告警路由,以及测试应该挂在模型的哪些位置。核心观点只有一个:验收标准要在开工前定,而不是出了问题再补。
传统数据项目的验收长这样:上线前导出几批数据,分析师肉眼扫一遍,没问题就交。这套做法的根本缺陷是验收动作不可重复——这次看过不等于下次还对,人眼扫不出来的问题(比如万分之一的重复主键)会一路漏到业务侧。
dbt 的思路是把验收变成代码。一个测试就是一条断言:「返回了任何行,测试就失败」。断言跟着模型走版本库,每次构建自动全量执行,成本接近于零。数据出问题时,你听到的第一个声音是测试报警,而不是业务方质问。
通用测试覆盖结构性校验,即「任何一张正经表都该满足」的性质。四个最常用的内置测试:
# 模型对应的配置文件:挂在 fct_orders 上的通用测试 models: - name: fct_orders columns: - name: order_id tests: - unique # 主键唯一 - not_null # 主键非空 - name: customer_id tests: - not_null - relationships: # 外键有效:必须能在维表里找到 to: ref('dim_customers') field: customer_id - name: order_status tests: - accepted_values: # 枚举合法 values: ['created', 'paid', 'shipped', 'done', 'closed'] - name: order_amount tests: - not_null
四个测试各盯一类缺陷:unique 抓重复(慢维变、重复同步的高发症),not_null 抓断档(上游改动字段后漏填的空值),relationships 抓孤儿行(事实表里指向不存在维度的记录),accepted_values 抓口径漂移(上游悄悄新增了一个状态码)。这四个测试的最大价值不在抓错本身,而在上游静默变更会被立刻暴露——上游加了一个订单状态,accepted_values 当场报错,你比业务方先知道口径变了。
**自定义测试(singular test)**覆盖业务规则——每家业务独有的、内置测试表达不了的约束。它就是一个普通的 SQL 文件,返回行即失败:
-- 自定义测试:已完成的订单必须存在支付记录 select o.order_id from {{ ref('fct_orders') }} o left join {{ ref('fct_payments') }} p on o.order_id = p.order_id where o.order_status = 'done' and p.payment_id is null
这条测试把一条业务规则(完成的订单必有支付)固化成断言。业务规则测试的积累速度是项目成熟度的真实指标:结构测试人人会写,业务测试写得多,说明团队把「踩过的坑」沉淀成了断言。给团队立一条规矩——每次数据事故的复盘,都要问一句「这个坑能不能变成一条测试」。
测试失败不都是同等紧急。dbt 允许给测试定严重级:error(默认,构建直接失败)与 warn(记录警告但放行)。分级的意义在于告警路由——什么级别的失败通知谁、阻断不阻断构建。
| 场景 | 级别 | 理由 |
|---|---|---|
| 主键唯一性被破坏 | error | 下游所有 JOIN 会翻倍,必须阻断 |
| 核心表字段非空被破坏 | error | 报表直接出错的根因 |
| 非关键维表出现少量孤儿行 | warn | 影响可控,白天人工处理 |
| 新增枚举值 | warn | 先放行并提醒,人工确认后更新测试 |
分级的原则是按下游影响半径定级,不按「错误听起来严重」定级。一条只影响某个次要看板的测试失败被设成 error,会反复阻断整个构建,最终结局是所有人学会无视报警——狼来了效应是测试体系最常见的死法。
背景:某项目的支付事实表有一条业务测试——「退款金额不得超过原支付金额」。某天上游支付系统上线新功能,部分场景允许「部分退款后全额退款」拆成两条记录,单条都不超原额,但同一笔支付的两条退款合计超了。
操作:当天构建,这条测试立即失败,报错返回 137 行。工程师没有直接改测试放行,而是先抽样核对了这批记录——确认是上游新行为,业务侧确认「合计不超原额」才是正确规则。于是改写测试:按支付单聚合后再与原额比对;改完重跑,通过。
结果:从上游变更发生到团队知晓,间隔不到一小时;从知晓到规则修正上线,又是两小时。整个过程业务方无感知。
解读:测试在这里起了两个作用——第一时间发现变更(否则这批数据会带着错误口径流进报表);强制团队把模糊的业务规则变成明确的文字(「单条不超」还是「合计不超」,测试逼着业务方给了答案)。变式思考:如果当时为了赶时间直接删掉测试,问题数据会在报表里安静地躺到有人对账那天,而那时候的排查成本是现在的十倍不止。
⚠️ 常见坑:测试覆盖率虚高。给三百个模型的每列都挂上 not_null,看起来覆盖很全,实际抓不到任何业务问题,还拖慢构建。测试要集中在三类位置:各表的主键、进入报表层的字段、踩过坑的规则。测试不是勋章,是保险——保额要花在真正的风险上。
自动化测试覆盖的是「可写成断言的规则」,验收体系里还有两类半自动的补充手段,各自有不可替代的位置。
采样核对用于新模型上线或大改动之后:人工抽几十行,从源表手工推算一遍关键字段,与模型产出比对。它抓的是「逻辑理解错误」——测试断言写错了和逻辑写错了一样安静,只有人的独立推算能发现「结论从根上就不对」。电商复盘一节的「层验收」用的就是它。
对账模型用于存量风险兜底:写一个专门的对比模型,把新旧两版产出(或源表与目标表)全量外连接,返回不一致的行。它本质上是把「测试」和「手工比对」合体——逻辑是手工核对的严谨版,执行是测试的自动化版。迁移期(1.3 节)与增量改造期(2.3 节)它的价值最大,平时可以降频为每周跑一次。
三类手段的分工可以一句话记住:测试管日常、采样管上线、对账管风险期。 三者共用同一套底层材料——依赖图、口径描述、行数基准——这也解释了为什么治理做得好的团队,验收体系会自然长好:材料是同一份。
验收标准齐了,材料与图纸也齐了——下一节把这一切装进一个真实的电商项目,从一页需求单走到上线。