关于 adding-dbt-unit-test
用于为 dbt SQL 模型定义和运行单元测试的技能,通过模拟输入并检查预期输出,帮助数据开发者在模型物化前验证转换逻辑与边界情况。
主要功能
- 01模型、输入与输出测试结构
- 02模拟源、种子和模型输入
- 03dict、CSV 与 SQL 夹具格式
- 04增量模型测试指引
- 05使用 dbt build 或 dbt test 运行
适用场景
- 01检查 SQL 转换场景和边界情况
- 02验证已报告问题的修复
- 03重构前测试逻辑
- 04验证高关键性模型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dbt-labs/ dbt-agent-skills --skill adding-dbt-unit-test安装后在 Claude Code 中输入/adding-dbt-unit-test确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/examples.mdspec.mdspecial-cases-ephemeral-dependency.mdspecial-cases-incremental-model.mdspecial-cases-special-case-overrides.mdspecial-cases-versioned-model.mdwarehouse-bigquery-caveats.mdwarehouse-bigquery-data-types.mdwarehouse-postgres-data-types.mdwarehouse-redshift-caveats.mdwarehouse-redshift-data-types.mdwarehouse-snowflake-data-types.mdwarehouse-spark-data-types.md详细介绍
为 dbt 模型添加单元测试
更多资源
- 规范参考 - 单元测试所需和可选的所有 YAML 键
- 示例 - 不同格式(dict、csv、sql)的单元测试示例
- 增量模型 - 对增量模型进行单元测试
- 临时依赖 - 对依赖临时模型的模型进行单元测试
- 特殊情况覆盖 - 内省宏、项目变量、环境变量
- 版本化模型 - 对版本化 SQL 模型进行单元测试
- BigQuery 注意事项 - BigQuery 特有的注意事项
- BigQuery 数据类型 - BigQuery 数据类型处理
- Postgres 数据类型 - Postgres 数据类型处理
- Redshift 注意事项 - Redshift 特有的注意事项
- Redshift 数据类型 - Redshift 数据类型处理
- Snowflake 数据类型 - Snowflake 数据类型处理
- Spark 数据类型 - Spark 数据类型处理
dbt 中的单元测试是什么
dbt 单元测试会基于静态输入验证 SQL 建模逻辑,然后再将模型物化到生产环境。如果模型的任何单元测试失败,dbt 都不会物化该模型。
适用场景
以下情况建议对模型进行单元测试:
- 为模型的预期功能以及边界情况添加模型-输入-输出测试场景,以防日后更改模型逻辑时引入回归问题。
- 验证错误修复是否解决了针对现有 dbt 模型提交的问题。
更多示例:
- SQL 中包含复杂逻辑时:
- 正则表达式
- 日期运算
- 窗口函数
- 存在多个
when的case when语句 - 截断
- 复杂联接(多个联接、自联接或带有复杂条件的联接)
- 编写用于处理输入数据的自定义逻辑时,类似于创建函数。
- 曾经报告过问题的逻辑。
- 实际数据中尚未出现,但希望确认能正确处理的边界情况。
- 重构转换逻辑之前(尤其是大幅重构)。
- “关键性”较高的模型(公开模型、受合同约束的模型,或直接位于某个暴露对象上游的模型)。
不适用场景
不建议创建单元测试的情况:
- 数据仓库提供方已进行充分测试的内置函数。如果出现意外问题,更可能是底层数据有问题,而不是函数本身。因此,单元测试中的固定数据无法提供有价值的信息。
min()等常见 SQL 规范函数
常规格式
dbt 单元测试包含模型、给定输入和预期输出三部分(模型-输入-输出):
model- 构建此模型时given输入 - 给定一组源、种子和模型作为前置条件expect输出 - 然后将模型中这些行的内容作为后置条件进行预期
工作流
1. 选择要测试的模型
顾名思义,就是选择要测试的模型!
2. 模拟输入
- 为模型依赖的每个节点创建一个输入。
- 指定要使用的模拟数据。
- 如果使用的
format不同于默认值(YAMLdict),请指定format。- 请参阅下方的“单元测试的数据
format”部分,确定要使用哪种format。
- 请参阅下方的“单元测试的数据
- 模拟数据只需包含此测试用例中使用的列子集。
**提示:**使用 dbt show 探索上游模型或源中的现有数据,帮助你了解真实的输入结构。不过,在将示例数据用于单元测试夹具之前,务必先清理数据,移除任何敏感信息或个人身份信息(PII)。
# Preview upstream model data
dbt show --select upstream_model --limit 5
3. 模拟输出
- 指定根据这些输入,预期模型生成的数据。
- 如果使用的
format不同于默认值(YAMLdict),请指定format。- 请参阅下方的“单元测试的数据
format”部分,确定要使用哪种format。
- 请参阅下方的“单元测试的数据
- 模拟数据只需包含此测试用例中使用的列子集。
4. 运行前确保上游模型已存在
单元测试要求直接父模型已存在于数据仓库中。 独立运行单元测试(dbt test)前,请先确认上游模型已存在:
# Check if upstream models exist in the warehouse
dbt list --select +my_model --exclude my_model --resource-type model
# Then verify the tables/views actually exist in the warehouse via dbt show or your SQL client
dbt show --select upstream_model --limit 1
如果上游模型不存在,或虽已存在但已修改且尚未刷新,请使用 --empty 构建它们,以创建仅包含架构的版本:
# Build upstream models cheaply (schema only, no data read)
dbt run --select +my_model --exclude my_model --empty
警告:
--empty会将现有模型覆盖为仅包含架构的版本(零行)。仅当模型尚不存在或需要应用架构变更时使用。如果上游模型中有需要保留的数据,请勿使用此选项,因为它会清除这些数据。
如果使用 dbt build --select my_model(推荐),请跳过此步骤,因为它会处理整个流水线,包括单元测试。
最简单元测试
假设你有以下模型:
-- models/hello_world.sql
select 'world' as hello
该模型的最简单元测试:
# models/_properties.yml
unit_tests:
- name: test_hello_world
# Always only one transformation to test
model: hello_world
# No inputs needed this time!
# Most unit tests will have inputs -- see the "real world example" section below
given: []
# Expected output can have zero to many rows
expect:
rows:
- {hello: world}
执行单元测试
运行单元测试、构建模型,并对 hello_world 模型运行数据测试:
dbt build --select hello_world
这样可以节省数据仓库开销,因为只有单元测试成功后,模型才会被物化并继续运行数据测试。
或者,只运行单元测试,不构建模型,也不运行数据测试:
dbt test --select "hello_world,test_type:unit"
也可以按名称选择特定的单元测试:
dbt test --select test_is_valid_email_address
从生产构建中排除单元测试
dbt Labs 强烈建议仅在开发或 CI 环境中运行单元测试。由于单元测试的输入是静态的,因此无需在生产环境中额外消耗计算资源来运行这些测试。在开发时,可将其用于测试驱动的开发方式;在 CI 中,可借此确保变更不会导致测试失败。
使用 --resource-type 标志 --exclude-resource-type 或环境变量 DBT_EXCLUDE_RESOURCE_TYPES,从生产构建中排除单元测试,以节省计算资源。
更贴近实际的示例
unit_tests:
- name: test_order_items_count_drink_items_with_zero_drinks
description: >
Scenario: Order without any drinks
When the `order_items_summary` table is built
Given an order with nothing but 1 food item
Then the count of drink items is 0
# Model
model: order_items_summary
# Inputs
given:
- input: ref('order_items')
rows:
- {
order_id: 76,
order_item_id: 3,
is_drink_item: false,
}
- input: ref('stg_orders')
rows:
- { order_id: 76 }
# Output
expect:
rows:
- {
order_id: 76,
count_drink_items: 0,
}
如需了解更多单元测试示例,请参阅 references/examples.md
支持和不支持的场景
- dbt 仅支持对 SQL 模型进行单元测试。
- 不支持对 Python 模型进行单元测试。
- 不支持对快照、种子、源、分析等非模型节点进行单元测试。
- dbt 仅支持为_当前_项目中的模型添加单元测试。
- 不支持对跨项目模型或从包导入的模型进行单元测试。
- dbt _不_支持对使用
materialized view物化方式的模型进行单元测试。 - dbt _不_支持对使用递归 SQL 的模型进行单元测试。
- dbt _不_支持对使用内省查询的模型进行单元测试。
- dbt _不_支持为增量模型指定
expect输出,以表示插入或合并后数据库表的最终状态。 - dbt _支持_为增量模型指定
expect输出,以表示将要合并或插入的数据。
值得了解
- 单元测试必须在
model-paths目录中的 YAML 文件里定义(默认为models/) - 单元测试的夹具文件必须在
test-paths目录中的 SQL 或 CSV 文件里定义(默认为tests/fixtures) - 在单元测试配置中,将所有
ref或source模型引用作为input,以避免编译时出现“找不到节点”错误。 - 如果模型有多个版本,默认情况下,单元测试会运行该模型的所有版本。
- 如果要对依赖临时模型的模型进行单元测试,必须对临时模型输入使用
format: sql。 - 模型中的表名必须使用别名,才能对
join逻辑进行单元测试
用于指定单元测试的 YAML
- 有关单元测试 YAML 定义中所有必需和可选的键,请参阅 references/spec.md
单元测试的输入
在单元测试中使用 input 引用测试所针对的特定模型或源:
- 对于
input:,使用表示ref或source调用的字符串:ref('my_model')或ref('my_model', v='2')或ref('dougs_project', 'users')source('source_schema', 'source_name')
- 对于种子输入:
- 如果未为种子提供输入,我们会将种子的 CSV 文件用作输入。
- 如果为种子提供了输入,我们会改用该输入。
- 将行设置为空列表
rows: [],以使用“空”输入- 如果模型有
ref或source依赖项,但其值与此单元测试无关,这种方式就很有用。但请注意,如果模型在该输入上执行联接,可能会导致行被筛除!
- 如果模型有
models/schema.yml
unit_tests:
- name: test_is_valid_email_address # this is the unique name of the test
model: dim_customers # name of the model I'm unit testing
given: # the mock data for your inputs
- input: ref('stg_customers')
rows:
- {email: [email protected], email_top_level_domain: example.com}
- {email: [email protected], email_top_level_domain: unknown.com}
- {email: badgmail.com, email_top_level_domain: gmail.com}
- {email: missingdot@gmailcom, email_top_level_domain: gmail.com}
- input: ref('top_level_email_domains')
rows:
- {tld: example.com}
- {tld: gmail.com}
- input: ref('irrelevant_dependency') # dependency that we need to acknowlege, but does not need any data
rows: []
...
单元测试的数据 format
dict 是默认格式 — 请始终从这里开始
除非有特定理由使用其他格式,否则请使用 dict(内联 YAML)。省略 format: 时,默认使用该格式。
given:
- input: ref('orders')
# no format: key needed — dict is the default
rows:
- {order_id: 1, status: completed, amount: 100}
- {order_id: 2, status: pending, amount: 50}
如何选择 format
使用 dict(默认) | 使用 sql | 使用 csv |
|---|---|---|
| 其他所有情况——从这里开始 | 模型依赖临时模型 | 使用外部夹具文件 |
| 列数据类型不受 dict/csv 支持 | 列数据类型不受 dict 支持 | |
| 外部夹具文件包含不受支持的类型 |
关于 sql 的重要说明:sql 格式要求在模拟数据中指定所有列。dict 和 csv 只要求提供与测试相关的列——简洁得多。
关于 sql 的重要要求:如果模型的任何 ref() 或 source() 输入是临时模型,必须对这些输入使用 sql 格式。dict 和 csv 会失败。
dbt 在单元测试中支持三种模拟数据格式:
dict(默认):内联 YAML 字典值。csv:内联 CSV 值或 CSV 文件。sql:内联 SQL 查询或 SQL 文件。
有关每种格式的示例,请参阅 references/examples.md
注意:
sql格式必须为所有列提供模拟数据,而dict和csv可以只提供部分列。- 只有
sql格式支持对依赖临时模型的模型进行单元测试——在这种情况下不能使用dict和csv。 - 没有任何格式支持 Jinja。
夹具文件
dict 格式只支持内联 YAML 模拟数据,但你也可以使用内联方式或单独的夹具文件来提供 csv 或 sql 数据。将夹具文件存放在任意 test-paths 下的 fixtures 子目录中。例如:tests/fixtures/my_unit_test_fixture.sql。
使用 dict 或 csv 格式时,只需为与你相关的列定义模拟数据。这样就能编写简洁且_有针对性_的单元测试。使用 sql 格式时,必须定义所有列。
特殊情况
- 增量模型的单元测试。请参阅 references/special-cases-incremental-model.md。
- 依赖临时模型的模型的单元测试。请参阅 references/special-cases-ephemeral-dependency.md。
- 依赖任何内省宏、项目变量或环境变量的模型的单元测试。请参阅 references/special-cases-special-case-overrides.md。
- 版本化 SQL 模型的单元测试。请参阅 references/special-cases-versioned-model.md。
平台/适配器特定注意事项
如果要在特定平台上实现(Redshift、BigQuery 等),需要注意一些平台特定的细节。请阅读适用于你所用数据库的注意事项文件(如果存在):
平台/适配器特定数据类型
单元测试旨在测试预期的 值,而非数据类型本身。dbt 会获取你提供的值,并尝试将其转换为根据输入和输出模型推断出的数据类型。
在单元测试 YAML 定义中指定输入值和预期值的方式在不同数据仓库之间大致一致,但较复杂的数据类型会有所不同。
请阅读适用于你所用数据库的数据类型文件:
- references/warehouse-bigquery-data-types.md
- references/warehouse-postgres-data-types.md
- references/warehouse-redshift-data-types.md
- references/warehouse-snowflake-data-types.md
- references/warehouse-spark-data-types.md
禁用单元测试
默认情况下,所有指定的单元测试均已启用,并会根据 --select 标志纳入执行范围。
如要禁用某个单元测试,请设置:
config:
enabled: false
如果单元测试因错误而失败,在修复之前需要先将其禁用,此时这项设置会很有用。
单元测试失败时
单元测试失败时,日志会显示“actual differs from expected”,并展示两者之间的“数据差异”:
actual differs from expected:
@@ ,email ,is_valid_email_address
→ ,[email protected],True→False
,[email protected],False
单元测试失败主要有以下两种可能:
- 单元测试的构造方式有误(假阳性)
- 模型存在错误(真阳性)
判断属于哪一种需要专业经验。
--empty 标志
在执行单元测试之前,被测模型的直接上游模型必须已存在于数据仓库中。run 和 build 命令支持使用 --empty 标志来执行仅构建架构的试运行。--empty 标志会将 ref 和 source 的行数限制为零。dbt 仍会针对目标数据仓库执行模型 SQL,但会避免读取成本高昂的输入数据。这可以验证依赖关系,并确保模型能够正常构建。
使用 --empty 标志构建空版本的模型,以节省数据仓库开销。
dbt run --select "stg_customers top_level_email_domains" --empty
常见错误
| 错误 | 修正方法 |
|---|---|
| 测试使用内置函数的简单 SQL | 仅对复杂逻辑进行单元测试:正则表达式、日期运算、窗口函数、多条件 case 语句 |
| 模拟输入数据中的所有列 | 仅包含与测试用例相关的列 |
在 dict 可用时使用 sql 格式 | 优先使用 dict(最易读);仅在必要时改用 csv 或 sql |
缺少 ref 或 source 的 input | 包含所有模型依赖,以避免出现“node not found”错误 |
| 测试 Python 模型或快照 | 单元测试仅支持 SQL 模型 |