数据库
在 dbt 中为 SQL 模型添加单元测试(TDD…
当需要为 dbt SQL 模型验证建模逻辑、修复 bug 或重构前建立回归保护时,用 YAML 定义 mock 输入与期望输出的单元测试。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 dbt 中为 SQL 模型添加单元测特定
解决思路
- 按 Model-Inputs-Outputs 三段式编写单元测试:model 指定要测试的模型,given 提供 source/seed/模型的 mock 输入作为前置条件,expect 声明模型输出的期望行内容作为后置条件。
- 选择要测试的模型:优先覆盖复杂逻辑(正则、日期运算、窗口函数、多 when 的 case when、截断、多表 join/自连接/非平凡 join 条件)、自定义数据处理逻辑、曾出过 bug 的逻辑、尚未在真实数据中出现的边界情况、重构前(尤其大重构)、以及高关键性模型(public、contracted 或直接位于 exposure 上游的模型)。
- 为模型依赖的每个节点创建 input,指定 mock 数据;默认使用 dict 格式,如需其他格式显式指定 format;mock 数据只需包含该测试用例用到的列子集。
- 可用 `dbt show --select upstream_model --limit 5` 查看上游模型或 source 的真实数据结构,但使用前必须脱敏,移除敏感或 PII 信息。
- 指定期望输出数据(expect),同样只需包含该测试用例用到的列子集,并按需指定 format。
- 运行单元测试前确保上游模型已存在于数仓中:先用 `dbt list --select +my_model --exclude my_model --resource-type model` 列出上游模型,再用 `dbt show --select upstream_model --limit 1` 确认表/视图真实存在。
- 若上游模型不存在或已修改但未刷新,用 `dbt run --select +my_model --exclude my_model --empty` 以 schema-only(零行)方式廉价构建;注意 `--empty` 会用零行版本覆盖已有模型,仅在模型尚不存在或需应用 schema 变更时使用,不要用于含需保留数据的模型。
- 若使用 `dbt build --select my_model`(推荐)则跳过上述上游检查步骤,它会自动处理包含单元测试的完整流程。
- 执行测试:用 `dbt build --select hello_world` 一次完成单元测试、构建模型和数据测试(单元测试通过才会物化模型,节省数仓开销);或仅跑单元测试用 `dbt test --select "hello_world,test_type:unit"`;或按名称跑单个测试 `dbt test --select test_is_valid_email_address`。
- 生产构建中排除单元测试:用 `--exclude-resource-type` 或环境变量 `DBT_EXCLUDE_RESOURCE_TYPES` 排除,仅在开发或 CI 环境运行单元测试以节省计算资源。
- 选择数据 format:默认用 dict(内联 YAML,最易读);模型依赖 ephemeral 模型或列数据类型不被 dict/csv 支持时用 sql;使用外部 fixture 文件或列类型不被 dict 支持时用 csv。
- 注意 sql 格式必须为所有列提供 mock 数据,而 dict 和 csv 只需提供相关列;若任一 ref()/source() 输入是 ephemeral 模型,该输入必须使用 sql 格式,dict 和 csv 会失败。
- fixture 文件放在任意 test-paths 下的 fixtures 子目录(如 tests/fixtures/my_unit_test_fixture.sql);单元测试必须定义在 model-paths 目录(默认 models/)下的 YAML 文件中。
- 在单元测试配置中为所有 ref 或 source 引用提供 input,避免编译时出现 "node not found" 错误;对需要声明但数据无关的依赖可用空输入 `rows: []`,但要注意若模型在该输入上有 join 会导致行被丢弃。
- seed 输入:若不提供 input,dbt 会直接使用 seed 的 CSV 文件作为输入;若提供 input,则使用提供的输入。
- input 中 ref/source 的写法:`ref('my_model')`、`ref('my_model', v='2')`、`ref('dougs_project', 'users')` 或 `source('source_schema', 'source_name')`。
- 模型有多个版本时,默认单元测试会在所有版本上运行;如需指定版本用 `ref('my_model', v='2')`。
- 模型依赖 ephemeral 模型时,必须对该 ephemeral 模型输入使用 `format: sql`。
- 要单元测试 join 逻辑时,模型内的表名必须使用别名。
- 禁用某个单元测试:在配置中设置 `config: enabled: false`,适用于测试错误失败需临时禁用的场景。
- 单元测试失败时查看 "actual differs from expected" 日志中的数据 diff,判断是测试构造错误(假阳性)还是模型 bug(真阳性)。
- 处理特殊场景时参考对应文档:增量模型、ephemeral 依赖、introspective 宏/项目变量/环境变量覆盖、版本化模型,以及各平台(BigQuery、Redshift、Snowflake、Postgres、Spark)的 caveats 与数据类型说明。
适用边界 · 注意事项
- 不要为数仓厂商已充分测试的内置函数(如 min() 等常见 SQL 标准函数)编写单元测试,此时 fixture 数据无法提供有价值信息。
- 不要测试 Python 模型、snapshot、seed、source、analysis 等非模型节点,也不支持跨项目或来自 package 的模型、materialized view 物化、递归 SQL、introspective 查询。
- 不要为增量模型插入/合并后数据库表的最终状态写 expect 输出(仅支持对将要合并/插入的内容写 expect)。