mcpField
A
adding-dbt-unit-test

adding-dbt-unit-test

github.com/dbt-labs测试与质量7471.1k 次安装1 周前更新
代码仓库

关于 adding-dbt-unit-test

用于为 dbt SQL 模型定义和运行单元测试的技能,通过模拟输入并检查预期输出,帮助数据开发者在模型物化前验证转换逻辑与边界情况。

主要功能

  1. 01模型、输入与输出测试结构
  2. 02模拟源、种子和模型输入
  3. 03dict、CSV 与 SQL 夹具格式
  4. 04增量模型测试指引
  5. 05使用 dbt build 或 dbt test 运行

适用场景

  1. 01检查 SQL 转换场景和边界情况
  2. 02验证已报告问题的修复
  3. 03重构前测试逻辑
  4. 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.md
spec.md
special-cases-ephemeral-dependency.md
special-cases-incremental-model.md
special-cases-special-case-overrides.md
special-cases-versioned-model.md
warehouse-bigquery-caveats.md
warehouse-bigquery-data-types.md
warehouse-postgres-data-types.md
warehouse-redshift-caveats.md
warehouse-redshift-data-types.md
warehouse-snowflake-data-types.md
warehouse-spark-data-types.md

详细介绍

为 dbt 模型添加单元测试

更多资源

dbt 中的单元测试是什么

dbt 单元测试会基于静态输入验证 SQL 建模逻辑,然后再将模型物化到生产环境。如果模型的任何单元测试失败,dbt 都不会物化该模型。

适用场景

以下情况建议对模型进行单元测试:

  • 为模型的预期功能以及边界情况添加模型-输入-输出测试场景,以防日后更改模型逻辑时引入回归问题。
  • 验证错误修复是否解决了针对现有 dbt 模型提交的问题。

更多示例:

  • SQL 中包含复杂逻辑时:
    • 正则表达式
    • 日期运算
    • 窗口函数
    • 存在多个 when 的 case when 语句
    • 截断
    • 复杂联接(多个联接、自联接或带有复杂条件的联接)
  • 编写用于处理输入数据的自定义逻辑时,类似于创建函数。
  • 曾经报告过问题的逻辑。
  • 实际数据中尚未出现,但希望确认能正确处理的边界情况。
  • 重构转换逻辑之前(尤其是大幅重构)。
  • “关键性”较高的模型(公开模型、受合同约束的模型,或直接位于某个暴露对象上游的模型)。

不适用场景

不建议创建单元测试的情况:

  • 数据仓库提供方已进行充分测试的内置函数。如果出现意外问题,更可能是底层数据有问题,而不是函数本身。因此,单元测试中的固定数据无法提供有价值的信息。
    • min() 等常见 SQL 规范函数

常规格式

dbt 单元测试包含模型、给定输入和预期输出三部分(模型-输入-输出):

  1. model - 构建此模型时
  2. given 输入 - 给定一组源、种子和模型作为前置条件
  3. expect 输出 - 然后将模型中这些行的内容作为后置条件进行预期
工作流
1. 选择要测试的模型

顾名思义,就是选择要测试的模型!

2. 模拟输入
  • 为模型依赖的每个节点创建一个输入。
  • 指定要使用的模拟数据。
  • 如果使用的 format 不同于默认值(YAML dict),请指定 format。
    • 请参阅下方的“单元测试的数据 format”部分,确定要使用哪种 format。
  • 模拟数据只需包含此测试用例中使用的列子集。

**提示:**使用 dbt show 探索上游模型或源中的现有数据,帮助你了解真实的输入结构。不过,在将示例数据用于单元测试夹具之前,务必先清理数据,移除任何敏感信息或个人身份信息(PII)。

# Preview upstream model data
dbt show --select upstream_model --limit 5
3. 模拟输出
  • 指定根据这些输入,预期模型生成的数据。
  • 如果使用的 format 不同于默认值(YAML dict),请指定 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 在单元测试中支持三种模拟数据格式:

  1. dict(默认):内联 YAML 字典值。
  2. csv:内联 CSV 值或 CSV 文件。
  3. 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 格式时,必须定义所有列。

特殊情况

平台/适配器特定注意事项

如果要在特定平台上实现(Redshift、BigQuery 等),需要注意一些平台特定的细节。请阅读适用于你所用数据库的注意事项文件(如果存在):

平台/适配器特定数据类型

单元测试旨在测试预期的 值,而非数据类型本身。dbt 会获取你提供的值,并尝试将其转换为根据输入和输出模型推断出的数据类型。

在单元测试 YAML 定义中指定输入值和预期值的方式在不同数据仓库之间大致一致,但较复杂的数据类型会有所不同。

请阅读适用于你所用数据库的数据类型文件:

禁用单元测试

默认情况下,所有指定的单元测试均已启用,并会根据 --select 标志纳入执行范围。

如要禁用某个单元测试,请设置:

    config: 
      enabled: false

如果单元测试因错误而失败,在修复之前需要先将其禁用,此时这项设置会很有用。

单元测试失败时

单元测试失败时,日志会显示“actual differs from expected”,并展示两者之间的“数据差异”:

actual differs from expected:

@@ ,email           ,is_valid_email_address
→  ,[email protected],True→False
   ,[email protected],False

单元测试失败主要有以下两种可能:

  1. 单元测试的构造方式有误(假阳性)
  2. 模型存在错误(真阳性)

判断属于哪一种需要专业经验。

--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 模型

评论

支持 Markdown