业务测试:把报表口径写成检查
目标:编写返回违规行的 SQL 测试,检查负金额和跨模型金额一致性。
上一课的通用测试能验证结构,却不知道“completed 才计入消费”。如果有人把 returned 也计入报表,订单编号仍然唯一,客户关系仍然有效,所有通用测试可能继续通过。本课给业务规则添加自己的反例查询。
singular data test 是 tests/ 下的一份 SQL 文件。它和模型都写 select,但含义相反:模型返回我们要交付的数据,测试返回不该出现的数据。默认测试结果为零行才通过。不要写 select count(*) 直接当测试,即使数值为零,它仍返回一行。
第一条规则:订单金额不能为负
创建 tests/assert_nonnegative_order_amount.sql:
select order_id, amount_cents
from {{ ref('stg_orders') }}
where amount_cents < 0
这是本店样本的口径:退货用状态表示,原订单金额保持非负。其他系统可能用负数表达退款,那就需要另一条业务规则,不能生搬硬套。
第二条检查把报表金额与原始业务口径对账。创建 tests/assert_revenue_reconciles.sql:
with expected as (
select coalesce(sum(amount_cents), 0) as cents
from {{ ref('stg_orders') }}
where status = 'completed'
),
actual as (
select coalesce(sum(revenue_cents), 0) as cents
from {{ ref('customer_summary') }}
)
select expected.cents as expected_cents, actual.cents as actual_cents
from expected
cross join actual
where expected.cents <> actual.cents
两个 CTE 各产生一个数字,cross join 把这两个一行结果并排放到一行。<> 表示不相等,只有不一致才返回。这里用整数分比较,避免将浮点近似误差误判成业务错误。coalesce 也让没有成交订单时两边都按零处理。
先判断正常输入会返回几条违规行,再运行:
uv run dbt build --profiles-dir . --exclude resource_type:seed
uv run dbt test --profiles-dir . --select assert_nonnegative_order_amount assert_revenue_reconciles
预期两个自定义测试均 PASS。加上上一课的 13 项,此时共有 15 项测试。对账只能检查这一条总额规则;它不能证明每位客户的分配都正确,也不能证明数据是今天刚同步的。
故意改坏报表,再查看反例
- 在
customer_summary.sql中,把where status = 'completed'改为where status in ('completed', 'returned')。 - 运行下面两条命令。第二条是本次实验预期的失败。
uv run dbt run --profiles-dir . --select customer_summary
uv run dbt test --profiles-dir . --select assert_revenue_reconciles
- 单独预览报表总额:
uv run dbt show --profiles-dir . --inline "select sum(revenue_cents) as cents from {{ ref('customer_summary') }}"
应得到 6000 分,正确口径仍是 5200 分。失败不是坏事,它证明这条检查能够识别刚才引入的回归。
- 恢复 completed 筛选,重新执行本课开头的 build 和 test,两项均应通过。
独立练习:编写 tests/assert_all_customers_present.sql,找出 staging 中存在、但客户汇总中缺失的客户。先写反例查询,再展开答案。完成后可运行验证;为保持后续测试数量一致,验证后删除这个练习文件。
完整参考实现
select c.customer_id
from {{ ref('stg_customers') }} as c
left join {{ ref('customer_summary') }} as s
on c.customer_id = s.customer_id
where s.customer_id is null
uv run dbt test --profiles-dir . --select assert_all_customers_present
正常版本应 PASS。如果临时改回上一课讨论的 inner join,反例会包含客户 3、4。测试后务必恢复 left join 并重新 build;最后删除练习文件,只保留本课主线的两份测试。
若测试永远失败,检查是否返回所有“正确行”,或写了始终返回一行的聚合;若更改模型后测试没变,先 run/build,让数据库使用新模型。SQL 测试末尾不要加分号,dbt 可能把查询包在其他语句中执行。
本课保留两份主线 SQL 测试,报表仍只统计 completed。与第 7 课结合后,项目既检查输入结构,也检查关键业务口径。