想象一下,你投入了数周甚至数月时间在一个复杂的数据项目上,把大量时间和精力倾注到构建一个 robust solution 中,结果它刚部署到 production 不久就崩塌了。当你发现自己曾经非常确信的数据现在充满 errors 和 discrepancies 时,挫败感会不断累积。依赖这些 flawed data 的 end users 开始报告问题,动摇了你辛苦建立起来的信任基础。这是一种噩梦般的场景,可能会摧毁 Data Engineers 与其 consumers 之间的关系。
传统上,data quality testing 很大程度依赖 preproduction 阶段的 manual checks。然而,对于 analytics 来说,这种方式就是灾难配方。Applications 的格局一直处于变化之中,不断演进,并不断引入可能损害 data quality 的新 bugs。事实是,data quality checks 的可靠性只等同于它们上一次执行时的可靠性,这会给未被发现的问题留下空间,而这些问题可能在 downstream 造成严重破坏。这就是为什么在 Data Engineering,或者 Analytics Engineering 过程中,引入 automated 和 comprehensive testing 至关重要。
这时 dbt 登场了,它是 data quality testing 领域的 game-changer。dbt 超越了许多竞争者,因为它将 testing capabilities 无缝集成到 model build process 中。借助 dbt,你可以轻松加入大量 tests,用来评估 data transformations 的 integrity。那些无意中产生 flawed 或 inaccurate data 的日子将一去不复返,你也不再需要依赖 end users 来告诉你某个 process 已经 broken。
贯穿本书,我们一直强调 dbt 如何将 Software Engineering best practices 应用于 Data Engineering,而 testing 也不例外。正如 testing 在 Software Engineering 中对于确保 code functionality 至关重要一样,dbt 提供工具,确保我们设计的数据转换能够产出符合 business expectations 的 models。通过利用 dbt 的 testing features,你可以验证 data 的 correctness 和 accuracy,增强信任,并赋能 data consumers 基于可靠信息做出 informed decisions。
Why Run Tests?
在深入 dbt 的复杂细节之前,首先要理解为什么 testing 在构建 robust data warehouse 中发挥关键作用。无论使用什么 tool 或 service,comprehensive testing 都可以确保 code 的 resilience 和 accuracy,并带来许多好处。
首先,也是最重要的一点,testing 会让你对自己开发的 code 产生深层次的 comfort 和 confidence。通过对 code 进行 rigorous testing,你可以确信它能按预期工作,并能承受 real-world scenarios。这会在项目工作过程中形成一种积极且确定的心态。
Testing 也可以作为一种验证手段,用来确认你的 code 会随着时间推移继续按预期运行。在 data warehouse 的动态环境中,data sources 和 schema 可能会演进,因此 regular testing 变得非常必要,它能帮助识别和纠正可能出现的 issues 或 regressions。这可以确保你的 code 保持 reliable、stable,并能适应变化,最大限度减少 downstream data processing 和 analytics 中的 disruptions 或 inaccuracies。
除了提升 code reliability 之外,data warehouse 中的 testing 还在赋能 data consumers 基于 accurate data 做出 informed decisions 方面发挥关键作用。通过对 data 和 code 进行 system tests,你可以为平台建立强大的 trust 和 credibility 基础。反过来,这会让 data consumers 对他们正在使用的数据质量有信心,从而促进 meaningful insights 和 well-informed decisions。
此外,记录这些 tests 不仅可以作为未来 developers 的 guide,也能鼓励对 codebase 的 collaboration 和 contributions。通过提供清晰且全面的 documentation,你可以促进自己和他人参与维护和增强 codebase。这会形成 shared ownership 的意识,也让基于已有工作继续构建变得更容易。总体而言,在 data warehouse 中运行 tests 会带来大量收益,使 testing 成为推动 data warehouse 整体成功和有效性的 indispensable practice。
构建 tests 时,无论使用什么 tool 或 service,都必须确保 tests 质量足够高。那么,什么构成了一个 good test?Data warehouse 中的 good test 具备几个关键 attributes,这些 attributes 共同决定了它的有效性和价值。正如 Ned Batchelder 精辟地说过,一个 good test 是 automated、fast、reliable、informative 和 focused 的。
Note
一个 good test 是 automated、fast、reliable、informative 和 focused 的。
首先,good test 是 automated 的,也就是执行所需 effort 很少,并且具有 repeatable 的性质。Automating testing process 可以显著减少运行 tests 所投入的时间和精力,从而释放资源用于其他关键任务。它们也应该作为你正在运行的 process 的一部分自动执行,而不是只局限于 post-runs。
Data warehouse 中的 good test 也应该是 fast 的,确保 testing phase 不会成为 development process 的 bottleneck。如果 tests 完成时间太长,developers 就不太可能优先处理它们,从而损害 codebase 的整体质量和可靠性。
Good test 的另一个 essential characteristic 是 reliability。Reliable test 会让 developers 对其结果的 accuracy 有信心,使他们能够信任 test 给出的 outcomes 和 assertions。当判断某件事是否工作时,对 test verdict 的信任至关重要,因为它能支持高效 troubleshooting 和 debugging。
此外,informative test 对定位 errors 或 unexpected behaviors 的 root cause 非常有价值。通过提供 meaningful error messages 或 clues,test 可以引导 developers 找到需要关注和修正的 specific areas。这种 informative 特征可以加速 debugging process,并提升 code maintenance 的效率和有效性。
除了上述 attributes,data warehouse 中的 good test 还应该体现 focus。每个 test 都应设计为验证 code 的一个 specific assumption 或 aspect。通过聚焦 single validation point,tests 会更加 targeted 和 precise,从而更容易识别问题并快速解决。这种 approach 也能提升 test suite 的 comprehensibility 和 maintainability,使 developers 在需要时更容易理解和修改 tests。
我们坚信,dbt 在其 framework 中非常擅长促进这些 attributes。通过利用 dbt,你可以在 data warehouse testing 中有效地自动化、加速、确保可靠性、提供信息丰富的反馈,并保持专注,从而优化整体 testing process,并为 data projects 的成功做出贡献。
Types of Testing in dbt
在 dbt 的世界中,tests 扮演着重要角色,因为它们是关于 dbt project 中 models 和 resources 的 assertions。这些 tests 像 gatekeepers 一样,确保你的 data 不仅 accurate,而且 consistent 和 reliable。当执行运行 tests 的 command,也就是 dbt test 时,dbt 会认真检查你的 tests,并明确告诉你它们是 pass 还是 fail。这个 development process 中的关键步骤保证了 data 的 integrity。
在 dbt 中,你会遇到两类不同 tests:singular tests 和 generic tests。Singular tests 是专门编写的 SQL queries,用于识别并返回 failing rows。这些 tests 旨在精准定位 data 中可能存在的 discrepancies 或 issues。另一方面,generic tests 采用 parameterized queries 的形式,可以接收 arguments。这种 flexibility 让你能够创建 dynamic tests,并在多个 scenarios 中复用,从而提升效率和 scalability。
Note
dbt 允许你构建两种类型的 tests:singular 和 generic。
接下来的 sections 中,我们会更详细地深入这两类 tests。通过探索它们的细微差异和复杂性,你将全面理解如何在 dbt 中有效利用 singular 和 generic tests,从而确保项目中的 data quality 和 confidence 达到最高水平。
Singular Tests
Singular tests 是 dbt 中最简单的 tests 形式。它们只是 SQL SELECT queries,写在 .sql files 中,并保存在 tests directory 下,用于返回 failing rows。这意味着,如果 query 返回任何结果,那么你的 test 就失败了。它们被称为 singular tests,因为它们只用于一个 single purpose。
来看一个简单示例。假设你有一张名为 sales 的 table,其中包含公司每笔 sale 的信息。在这张 table 中,有一个名为 price 的 column,表示每笔 sale 的价格。你知道每个 price 都应该是 positive number,因为你并不会倒贴钱让 customers 拿走产品。如果存在 negative number,那就说明存在 data quality issue,需要修复,而且你需要在 downstream users 和 processes 消费这些 data 之前知道这个问题。在这个例子中,你可以创建一个 singular test 来检查它。我们先写一个 SELECT query,用来告诉我们 price 是否小于 zero。见 Listing 8-1。
select
price
from {{ ref('sales') }}
where price < 0
Listing 8-1:检查 price 是否小于 zero 的 SELECT query
每当你运行这个 query,它要么不返回结果,也就是 test passes;要么返回结果,也就是 test fails。如果这个 query 返回结果,那么我们就知道 sales table 中存在需要修正的 negative prices。事情真的就是这么简单。
Singular tests 的写法和 models 非常相似。它们写成 SELECT statements,并且应该在 test definition 中使用 Jinja commands,包括 ref() 和 source() functions。它们与 models 最大的区别在于:tests 只存储在 tests directory 中,并且不像 models 那样被 materialized。你可以为任何需要测试的内容构建 singular tests。Test 的 success 或 failure,仅仅取决于它是否返回 results。
因为 singular tests 很容易写,你可能会发现自己经常使用它们。你甚至可能会注意到,自己正在反复为不同 tests 编写同一种基本结构,只是改变 column name 或 model reference。在这种情况下,你的 singular test 就不再那么 singular 了,你可能希望让它更加 generic。这正是 generic tests 发挥作用的地方。
Generic Tests
Generic tests 这个名字非常贴切,也准确描述了它们是什么。它们是可以反复复用的 generic tests。不同于通常服务于 single purpose 的 singular tests,generic tests 可以服务于 multiple purposes。它们包含 parameterized query,接收 arguments,因此具有很强的 flexibility。
首先看一下创建 generic test 所需的 syntax:
{% test <name of test> (argument 1, argument 2) %}
select
statement
from {{ argument 1 }}
where {{ argument 2 }} = X
{% endtest %}
{% test %} 和 {% endtest %} blocks 是必需的,这样 dbt 才知道这是一个 generic test。在 {% test %} block 中,你需要根据需要输入几个 fields。首先,需要定义 test 的 name。后面引用这个 test 时用的是这个 name,而不是像 dbt project 中其他 resources 那样使用实际 file name。此外,还需要添加你想使用的 arguments,也就是 parameters。最后,需要添加运行 test 的 select statement。如果你定义了 arguments,一定要确保它们被纳入 select statement,并用 curly brackets 包裹,从而符合 Jinja syntax。第 6 章已经回顾过 Jinja 的 essentials,那里学到的所有内容都可以迁移到 dbt tests 编写中。
现在你理解了 syntax,让我们使用 singular tests 中同一个例子,并把它放到 generic 的语境中。假设我们的 models 中还有几张其他 tables,包含不应为 negative 的 numbers。也许有 tables 包含 sales totals、tax、number of shoppers、number of payments used 等 values,而这些 values 也不应该为 negative。我们可以像为 sales 创建 singular tests 一样,为每一个场景都创建 singular tests,但那会产生大量重复代码。那如果我们只创建一个 generic test,让它执行相同 query,只接收不同的 arguments 呢?在这个案例中,不同的只是 column name 和 model name。我们还可以把 test name 设置为 not_negative。Listing 8-2 展示了代码会是什么样子。
{% test not_negative(model, column_name) %}
select
statement
from {{ model }}
where {{ column_name }} < 0
{% endtest %}
Listing 8-2:确保 value 不为 negative 的 generic test 示例
在这个示例中,我们将 test 命名为 not_negative。同时创建了两个 arguments:model 和 column_name。创建 generic tests 时,如果你要测试某个 column 中的 values,那么这个 test 至少应接收这两个 standard arguments。不过,你也可以传入 additional arguments。
然后,我们创建了一个 select query,将两个 arguments 纳入其中,并查找 negative values。现在这个示例就可以用于检查许多 model 和 column combinations 中是否存在 negative values,而不是用 singular test 多次编写相同 logic。能够创建 arguments,正是 generic test 之所以 generic 的原因。
这是一个非常简单的 generic test 示例,但你可以把它做得更 advanced。你可以根据需要自定义 arguments 和 select statements,以获得期望结果。关于 generic tests,有一个有趣点:model 和 column_name arguments 是特殊的。每当使用这些 arguments,并且在 YAML files 中配置 test 时,dbt 会自动为它们提供 values。只有这两个 arguments 会这样。其他所有 arguments 都需要你显式定义。
Note
每当你创建带有 model 和 column_name 作为 arguments 的 generic test,并在 YAML files 中配置它时,dbt 会自动提供这些 values。
不同于 singular tests,generic tests 需要添加到 sources.yml,针对 sources,以及 schema.yml files,针对 models,才能被使用。本章后面会介绍如何添加它们。
Out-of-the-Box Tests
dbt 自带一些 out-of-the-box generic tests,你无需创建任何东西即可使用。这些 tests 是几乎每个使用 dbt 的人都会在某个时刻需要使用的标准项。下一节中,我们会看看如何整合目前讨论过的所有 tests。
在 dbt 领域中,你有四个 predefined generic tests 可以立即开始使用。它们是 unique、not_null、accepted_values 和 relationships。这些 tests 随 dbt 一起打包提供,为验证和核验 data integrity 提供坚实基础。让我们逐一深入这些 tests,理解它们在 data testing 领域中的 purpose 和 technical significance。
Note
dbt 开箱即带四个 generic tests:unique、not_null、accepted_values 和 relationships。
第一个可用 test 是 unique test。它的主要目标是确保 model 中某个 column 的 uniqueness,检测是否存在 duplicate values。通过加入 unique test,data engineers 可以消除 redundant entries,从而维护 data 的 integrity 和 consistency。这个 test 通常用于 model 的 primary key。
继续往前,我们会遇到 not_null test。它是防止 column 中出现 NULL values 的 essential guardian。这个 test 像一个 robust gatekeeper,确保指定 column 不包含任何 null values。通过使用 not_null test,Data Engineers 可以加固 data pipelines,防止由于 missing values 引发 unexpected data gaps 和 ambiguities。
接下来是 accepted_values test。这个 powerful test 使 data engineers 能够在某个 column 中强制执行一个 predetermined list of accepted values。通过应用 accepted_values test,可以保证 data 遵循 specific criteria,从而促进 consistency,并遵守 predefined data quality standards。
最后是 relationships test,它是维护 referential integrity 的 invaluable tool。这个 test 会认真检查 foreign key values,验证它们是否与相关 references 对齐。通过加入 relationships test,Data Engineers 可以确保 data relationships 的 coherence 和 accuracy,防止 data processing 中出现 inconsistencies 和 errors。虽然这个 test 很有用,但我们建议只有在必要时使用它,因为在较大 datasets 上运行时,它可能计算成本较高。
这四个 generic tests:unique、not_null、accepted_values 和 relationships,是 dbt framework 中 data testing 的基石。它们为 data practitioners 提供了增强 data quality 的手段,确保准确且可靠的数据转换。接下来的 section 中,我们会探索如何将这些 tests 以及其他 testing methodologies 无缝集成到 dbt workflow 中。准备好释放 data quality assurance 的全部潜力,并把 data projects 推向更高层次的 reliability 和 trustworthiness。
Setting Up Tests
为了让 dbt 知道你需要运行某个 generic test,必须在 YAML files 中配置它。Singular tests 会作为 model builds 的一部分运行,但 generic tests 需要被告知要在哪些对象上运行。它们支持在 models、sources、seeds 和 snapshots 的 YAML property files 中配置。
Reminder
只有 generic tests 需要在 YAML files 中配置。Singular tests 会作为 model builds 的一部分运行。
来看图 8-1,其中展示了一个 model YAML file 示例,包含本章到目前为止描述的所有 tests 示例。这个示例把我们目前讨论过的所有 tests 都整合进一个 sample schema.yml file 中。
图 8-1:包含 tests 的 model YAML file 示例
在这个示例中,我们有一个名为 sales 的 model,其中包含四个 columns:sales_id、status、price 和 customer_id。对于这些 columns 中的每一个,我们都配置了不同 tests,用于展示它们如何工作。让我们逐列走读,更好理解其中发生了什么。
对于 sales_id column,我们让 dbt 运行两个 out-of-the-box tests:unique 和 not_null。这意味着 dbt 会检查 sales_id column,也就是我们的 primary key,确保其中所有 values 都是 unique,并且不包含 null values。如果这些 assertions 中任何一个不成立,那么 project build 就会 fail。
对于 status column,我们让 dbt 运行 accepted_values check。这是 dbt 提供的另一个 out-of-the-box test。它意味着 dbt 会检查该 column 的内容是否只包含以下 values:placed、shipped、completed 或 returned。如果任何 column 中存在与这些 values 不同的 value,那么 project build 就会 fail。
接下来是 price column。在这个 column 上,我们运行本章前面创建的 generic test,名为 not_negative。这个 custom check 会确保 price 的 value 不是 negative number。如果它发现 negative number,那么 project build 就会 fail。
最后是 customer_id column。在 sales table 中,customer_id 与 customer table 中的 customer_id column,也就是 primary key,存在 foreign key relationship。我们知道这张 table 的 customer_id column 中每个 value 都应该在 customer model 中有对应的 customer_id,所以希望检查这个 assumption。为此,我们会使用 built-in relationships test。这里我们将使用 Jinja ref() function 引用 customer model,并列出要检查的 field。如果这个 test 发现 sales table 中存在某些 customer_ids,而这些 IDs 不存在于 parent table 中,那么 build 就会 fail。
你可以运行的 tests 数量没有限制,因此可以在任何 column 上添加前面 tests 的任意组合。我们强烈建议充分利用 dbt builds 中的 tests,因为这确实是使用 dbt 的主要优势之一。
Configuring Test Severity
默认情况下,在 dbt 中,每个 test 执行时只要满足 test 的失败条件,就会产生 failure。即便只有一条 failing row,也会得到 failure。大多数时候,这可能正是你想要的。不过有些时候,你可能希望针对某些内容运行 tests,但只返回 warning;或者希望 test 根据返回的 failures 数量有条件地失败。幸运的是,dbt 可以实现这一点。
在 YAML files 中,有三个相关 configs 可以告知 dbt 如何处理这些情况。它们是 severity、error_if 和 warn_if。来看每个分别是什么:
severity
这个 config 允许你选择 test 的 severity,可以是 error 或 warn。error 表示 test 出错时会导致 build fail。warn 表示它会在 logs 中添加一条 message,但 build 会继续运行。这个 setting 的 default 是 error,因此如果没有指定,就使用该值。
error_if
这是一个 conditional expression,允许你指定触发 error 的 threshold。例如,如果你正在运行 duplicate check,并且只想在 duplicates 超过 10 条时得到 error,就可以使用这个 config 设置 threshold。默认情况下,这里的 value 是 zero,意味着只要有一条 duplicate,就会得到 error。
warn_if
这是一个 conditional expression,允许你指定触发 warning 的 threshold。例如,如果你正在运行 duplicate check,并且只想在 duplicates 超过 5 条时得到 warning,就可以使用这个 config 设置 threshold。默认情况下,这里的 value 是 zero,意味着只要有一条 duplicate,就会得到 warning。
这些 settings 可以应用在 dbt project 中的多个位置。它们可以应用到 generic tests、singular tests,以及 project level。让我们分别看看如何为每个 level 配置它们。
先从 generic tests 开始,看它在 schema.yml file 中会是什么样子。假设有一张 table 和一个 column,我们想配置 severity、error_if 和 warn_if。我们会在 test name 下面添加一个 config block,并加入每个选项。图 8-2 展示了示例。
图 8-2:包含 test severity 的 model YAML file 示例
Tip
我们经常看到将 severity 改为 warn 的地方,是在 sources.yml file 中运行的 tests。很多时候,你可能无法改变 upstream system 来修复 underlying issue,但你确实想知道这个问题正在发生。
对于 singular test,只需要在 select statement 顶部添加 config Jinja block 并定义 settings。例如,如果你希望 returned results 超过 10 条时 fail,可以添加:
{{
config(
error_if = '> 10'
)
}}
select
...
最后,default settings 可以在 dbt_project.yml file 中的 project level 配置。你可以为 project 的多个 levels 指定 severity、error_if 和 warn_if,包括 entire project、directories 和 packages。再次说明,默认情况下,对于 test 返回的任何 results,一切都设置为 error。下面示例展示了如何将整个 dbt project 的 default severity 改为 warn,以及如何设置一个 individual package,使其在返回 results 超过 10 条时 error:
tests:
+severity: warn
<package_name>:
+error_if: >10
关于如何配置 test severity,有许多选项,也可以应用在很多 layers。我们认为对大多数人来说,默认 behavior 已经足够,尤其是在你刚开始时。但当你越来越 advanced,并希望对 project 拥有更多 control 时,可能会遇到需要设置 severity 的场景。
Test Syntax
本章这一节将聚焦你实现 tests 时需要遵循的 YAML syntax。Tests 对 models、sources、seeds 和 snapshots 都有 supported configuration。开始构建 dbt assets 时,把这部分作为 reference 会很有帮助。图 8-3 展示了包含 tests 的 model YAML files 的 syntax。对于 models,tests 可以运行在 model level 或 column level。
图 8-3:models 的 test syntax
图 8-4 展示了包含 tests 的 source YAML files 的 syntax。对于 sources,tests 可以运行在 table level 或 column level。
图 8-4:sources 的 test syntax
图 8-5 展示了包含 tests 的 seed YAML files 的 syntax。对于 seeds,tests 可以运行在 seed level 或 column level。
图 8-5:seeds 的 test syntax
图 8-6 展示了包含 tests 的 snapshot YAML files 的 syntax。对于 snapshots,tests 可以运行在 snapshot level 或 column level。
图 8-6:snapshots 的 test syntax
所有可用的 testing options 都非常相似,只存在一些细微差异。Models、sources、seeds 和 snapshots 都支持 tests,但支持的 levels 不完全相同。刚开始时,理解有哪些选项非常重要。此外,也要理解 syntax 的 indentation 和 spacing。
Executing Tests
Tests 的运行方式与 dbt project 中其他 resources 类似,不过可用的 selection criteria 稍有不同。因此,你可以在 specific model 上运行 tests,也可以运行 specific directory 中所有 models 的 tests,或者运行某个 particular model upstream 或 downstream 的 models 上的 tests。关于实现 node selection 的完整不同方式,请回顾第 4 章。
首先从基础开始。dbt 有一个专门执行 tests 的 command,叫 dbt test。这是执行 tests 的 foundational dbt command。运行它时,project 中的所有 tests 都会运行。如果你希望更有选择性地运行 tests,可以在后面添加许多内容,用于精确指定你想做什么。本节后面会覆盖这些内容。在此之前,我们想提到,当你使用 dbt build 时,所有 tests 也会运行。
Note
Tests 也会作为 dbt build runs 的一部分运行。
与其他 resource types 类似,tests 可以通过 methods 和 operators 直接选择,这些 methods 和 operators 会捕获 tests 的某个 attribute,例如 name、properties、tags 等。与其他 resource types 不同的是,tests 也可以被 indirectly selected。如果某个 selection method 或 operator 包含 test 的 parent(s),test 本身也会被选中。这是运行 dbt test command 时特有的行为,被称为 eager selection。稍后会进一步讨论。
首先看 dbt test 可用的 node selection syntax。实际上,它与其他 dbt commands,例如 dbt run,完全相同,包括 --select、--exclude、--selector 和 --defer。第 4 章中已经详细覆盖过这些内容,这里简单回顾每个做什么:
--select
用于选择一个 model,或 models subset,对它们运行 tests。
--exclude
用于排除一个 model,或 models subset,不对它们运行 tests。
--selector
使用 YAML selector 运行 tests。
--defer
可以与 dbt 的 previous state,也就是 previous run,进行对比来运行 test。
还有四种 modes 会直接影响 dbt 运行 tests 时的 behavior。这些包括 dbt 如何解释刚才提到的 node selections。
Direct
在 dbt 中,你可以基于 name、tags 和 property types 等 attributes,直接选择想运行的 tests。
Indirect – eager
默认情况下,dbt 选择任何 parent resource 时,会通过 “eager” indirect selection approach 触发 test。这意味着当任何 parent 被选中时,所有关联 tests 都会执行,即使这些 tests 依赖其他 models。在这种 mode 下,如果 test 依赖尚未 build 的 resources,就会抛出 error,提示存在 unbuilt resources。
Indirect – cautious
这允许 users 基于 tests 的 parent resources 是否被选中,来控制 tests 是否执行。在这种模式下,tests 只有在所有 parent resources 都被选中并 build 时才会运行。换句话说,如果某个 test 的任何 parent 未被选中,这些 tests 就不会执行。这种 approach 确保 tests 被限制在其 references 落在 selected nodes 内的范围,防止执行带有 unselected parent resources 的 tests。
Indirect – buildable
这与 cautious mode 类似,但略微更 inclusive。这种 mode 会包含那些 references 被限制在 selected nodes 或其 direct ancestors 范围内的 tests。更广泛地包含 tests,在需要确认 aggregations 与其 input totals 相同的场景中很有价值,因为这涉及一个依赖 model 及其 direct ancestor 的 test。
来看一些如何运行 tests,以及它们以哪种 mode 工作的示例:
对 specific model 运行 tests(indirect):
dbt test --select customers
对 models/marts/sales directory 中所有 models 运行 tests(indirect):
dbt test --select mart.sales
运行某个 model downstream 的 tests(direct)。Model name 后面的 + 用来告诉 dbt 运行 downstream models:
dbt test --select stg_customers+
运行某个 model upstream 的 tests(indirect)。Model name 前面的 + 用来告诉 dbt 运行 upstream models:
dbt test --select +stg_customers
运行所有带 Sales tag 的 models 上的 tests(direct and indirect):
dbt test --select tag:Sales
运行所有 table materialization 的 models 上的 tests(indirect):
dbt test --select config.materialized:table
Viewing Test Failures
每当运行 tests 时,我们期望一切都 pass。但如果某个 test failed,会发生什么?你怎么知道哪里失败了、该修复什么?其实不做任何额外事情,你也可以查看 dbt 的 debug logs,看到到底什么 failed。你会找到运行过的 SQL 和产生的 error,可以用它们 troubleshoot 并找出问题。如果你的 build 是作为 dbt Cloud job 的一部分运行并失败,那么仍然需要查看 dbt logs 来弄清发生了什么。图 8-7 展示了 dbt Cloud 中 failure 的示例。
图 8-7:dbt Cloud 中 test failure 示例
但还有另一种方式可以做到这一点。你可以使用 --store-failures flag,让 dbt 将 test failures 存储到 database 中。它会在 database 中创建一个 new schema,并为每个 test 创建一张 table,用于存储导致 failure 的 rows of data。它不会把所有 test failures 存到同一张 table,因为不同 test 的 outputs 会不同。这些 tables 也不会存储 historical values,而是每次运行时被 truncated and loaded。它们更多用于帮助 troubleshoot issue,而不是用于判断 failures over time。要做后者,你需要使用不同方法,例如第 7 章中讨论过的 solutions。
Note
要查看 test failures,你需要查看 dbt debug logs,或者让 results 写入 table。
来看如何使用 store failures feature。第一个示例展示如果通过 command line 运行 command,它会是什么样子:
dbt test --select my_model_name --store_failures
你也可以使用 config blocks 在 individual test level 设置它。只需将 store_failures value 设置为 true 或 false。默认情况下它是 false,因此不需要显式设置,除非你在 dbt_project.yml file 中将默认值设置为 true:
{{ config(store_failures = true) }}
你也可以在 model YAML files 中为 specific tests 设置这个 value,而不是使用 config blocks。例如:
此外,如果你想为整个 project,包括 models 和 packages,设置 default,也可以在 dbt_project.yml file 中设置这个 value。
这个配置可以从 project level 一直启用到 individual model layer,以及中间的所有层级。
Test Packages
正如本章多次提到的,dbt 开箱即带的 generic tests 只有四个。虽然它们都非常有用,但这只是可能性的冰山一角。你可以尽情构建自己想要的所有 tests;不过,在这么做之前,我们强烈建议先查看一些 open source packages。dbt-utils 和 dbt-expectations 是两个非常流行且有用的 packages。
在我们看来,每个 dbt user 都应该使用 dbt-utils package,因为它极其有用。它是由 dbt Labs 创建并维护的 package,因此你知道它是靠谱的。你应该使用它有很多原因,本书中也分享过几个;但现在我们只聚焦 testing 部分。截至本文写作时,这个 project 中包含 16 个 generic tests。来看一下它们是什么:
equal_rowcount:检查两个 models 是否拥有相同数量的 rows。
fewer_rows_than:检查相应 model 的 rows 数量是否少于被比较的 model。
equality:检查两个 models 之间是否相等。
expression_is_true:检查 SQL expression 的结果是否为 true。
recency:检查 reference model 中的 timestamp column 是否有不早于指定 date range 的 data。
at_least_one:检查某个 column 至少有一个 value。
not_constant:检查某个 column 是否并非所有 rows 都有相同 value。
not_empty_string:检查某个 column 是否不包含 empty string。
cardinality_equality:检查一个 column 中的 values 数量是否与另一个 model 中不同 column 的 values 数量完全相同。
not_null_proportion:检查某个 column 中 non-null values 的比例是否位于指定 range 内。
not_accepted_values:检查没有 rows 匹配所提供的 values。
relationships_where:检查两个 relations 之间的 referential integrity,但增加了 filtering 的能力。
mutually_exclusive_ranges:检查某一行定义的 ranges 是否不与另一行的 ranges 重叠。
sequential_values:检查某个 column 是否包含 sequential values。
unique_combination_of_columns:检查 columns 的组合是否唯一。
accepted_range:检查某个 column 的 values 是否落在 specific range 内。
这些 tests 中有一些非常强大,可以很容易添加到你的 project 中使用。当现成可用时,就没有必要 reinvent the wheel 来产出相同 tests。这个 package 不仅包含多个 additional tests,还支持在 tests 中 grouping,从而增加另一层 granularity。有些 data validations 需要 grouping 来表达 specific conditions,另一些 validations 在 per-group basis 上执行时结果会更准确。这只是 tests 执行方式中的一个额外 feature。
另一个你一定要查看的 package 是 dbt-expectations。这个 package 受到 Python 的 Great Expectations package 启发。它由 Calogica 创建和维护,Calogica 是一家位于 California 的 data and analytics consulting firm;我们认为它在 tests 方面极具价值。它提供了更多可用 tests,下面列出这些 tests。
Table shape
expect_column_to_existexpect_row_values_to_have_recent_dataexpect_grouped_row_values_to_have_recent_dataexpect_table_aggregation_to_equal_other_tableexpect_table_column_count_to_be_betweenexpect_table_column_count_to_equal_other_tableexpect_table_column_count_to_equalexpect_table_columns_to_not_contain_setexpect_table_columns_to_contain_setexpect_table_columns_to_match_ordered_listexpect_table_columns_to_match_setexpect_table_row_count_to_be_betweenexpect_table_row_count_to_equal_other_tableexpect_table_row_count_to_equal_other_table_times_factorexpect_table_row_count_to_equal
Missing values, unique values, and types
expect_column_values_to_be_nullexpect_column_values_to_not_be_nullexpect_column_values_to_be_uniqueexpect_column_values_to_be_of_typeexpect_column_values_to_be_in_type_listexpect_column_values_to_have_consistent_casing
Sets and ranges
expect_column_values_to_be_in_setexpect_column_values_to_not_be_in_setexpect_column_values_to_be_betweenexpect_column_values_to_be_decreasingexpect_column_values_to_be_increasing
String matching
expect_column_value_lengths_to_be_betweenexpect_column_value_lengths_to_equalexpect_column_values_to_match_like_patternexpect_column_values_to_match_like_pattern_listexpect_column_values_to_match_regexexpect_column_values_to_match_regex_listexpect_column_values_to_not_match_like_patternexpect_column_values_to_not_match_like_pattern_listexpect_column_values_to_not_match_regexexpect_column_values_to_not_match_regex_list
Aggregate functions
expect_column_distinct_count_to_be_greater_thanexpect_column_distinct_count_to_be_less_thanexpect_column_distinct_count_to_equal_other_tableexpect_column_distinct_count_to_equalexpect_column_distinct_values_to_be_in_setexpect_column_distinct_values_to_contain_setexpect_column_distinct_values_to_equal_setexpect_column_max_to_be_betweenexpect_column_mean_to_be_betweenexpect_column_median_to_be_betweenexpect_column_min_to_be_betweenexpect_column_most_common_value_to_be_in_setexpect_column_proportion_of_unique_values_to_be_betweenexpect_column_quantile_values_to_be_betweenexpect_column_stdev_to_be_betweenexpect_column_sum_to_be_betweenexpect_column_unique_value_count_to_be_between
Multicolumn
expect_column_pair_values_A_to_be_greater_than_Bexpect_column_pair_values_to_be_equalexpect_column_pair_values_to_be_in_setexpect_compound_columns_to_be_uniqueexpect_multicolumn_sum_to_equalexpect_select_column_values_to_be_unique_within_record
Distributional functions
expect_column_values_to_be_within_n_moving_stdevsexpect_column_values_to_be_within_n_stdevsexpect_row_values_to_have_data_for_every_n_datepart
在 dbt_utils 和 dbt_expectations 这两个 packages 之间,有大量 tests 机会可以满足许多需求。当然,总会存在由具体业务 test requirements 决定的 custom tests,但我们相信这已经覆盖了非常大的范围。要了解这两个 packages,并开始使用它们,可以查看 dbt package hub(hub.getdbt.com)或探索 dbt Slack community。
Other Packages
Testing 是使用 dbt 时的关键 component,因此有许多 open source resources 可以提供帮助。上一节中,我们介绍了几个最喜欢的 packages,但还有很多。虽然第 6 章已经详细覆盖了 packages,但我们想再次提到其中一些,在刚开始时值得一看。它们并不全是由 dbt Labs 创建和维护的;不过,这些都曾在官方 dbt documentation 中某处被提到过,也是我们用过的 packages。
dbt package hub(hub.getdbt.com)
dbt_meta_testing:用于强制确保 project 中的 models 拥有 required tests 的 package。
dbt_dataquality:访问并报告 dbt source freshness 和 test 的 outputs。只适用于 Snowflake。
dbt-audit-helper:用于对 diffs 执行 data audits 的 package。
GitHub packages
dbt-coverage:Python CLI tool,用于检查 dbt project 是否缺少 documentation 和 test。
dbt-checkpoint:Python package,通过 pre-commit checks 添加 automated validations 来改进 code。
dbt-project-evaluator:用于检查 dbt project 是否符合 dbt Labs best practices 的 package。
和任何直接从 Internet 拉取的东西一样,我们仍建议你谨慎使用。任何由 dbt Labs 管理和维护的东西通常都会没问题,但其他 packages 在立即集成到 production processes 之前应该经过一些评估。我们以前使用这些 packages 没有遇到问题,但这并不意味着未来绝不会出现问题。由于其中许多由 community members 或 third-party resources 维护,它们可能不会很快跟上 dbt product updates 或 bug fixes,所以如果遇到问题,要保持耐心和宽容。或者更好的是,修复问题并给它们提交 pull request!
Best Practices
当你开始为 models 构建 tests 时,我们有几个实用建议,相信能帮助你的旅程。这些是我们一路学习中总结出来的经验,我们认为它们能帮助你构建自己的 dbt testing framework。这些不是硬性要求,我们也理解它们未必适用于每种情况,但确实认为它们适用于大多数场景。
首先,我们建议从 generic tests 开始,并且只有在必须时才使用 singular tests。Generic tests 可以写一次,然后应用到多个 models 上,减少重复代码需求,从而节省 development 和 maintenance 的时间与精力。只有当你需要某个东西服务于 singular purpose 时,才使用 singular tests。
其次,我们强烈建议,在构建自己的 tests 之前,先查看并使用 open source test packages。像 dbt_utils 和 dbt_expectations 这样的 packages 是很好的首选位置,但也有其他 open source packages 可以利用。花时间构建可能已经存在的东西并没有太大价值。如果它不是你业务特有的内容,很可能已经存在了。
接下来,我们建议始终在构建 models 的同时构建 tests,而不是事后再做。模型和测试本就应该同步进行,这可能听起来显而易见,但你可能来自一个 post-processing 后才测试的背景,因为很多 systems 都是这样。Test-driven development 是另一个 software engineering best practice,而 dbt 为你提供了遵循它的工具。使用 dbt 时,你应该把 testing 当成 model creation 的一部分来思考。每当你更新 sources.yml files 或 schema.yml files,都应该养成习惯,把你对 data 作出的 assertions 加入 tests 中。
最后,我们建议测试你的 data sources。这可以确保在你开始在 raw data 上叠加 models 之前,你对 raw data 作出的 assertions 是成立的。我们建议将 source tests 的 severity 设置为 warning,这样它不会阻止 transformations 运行。但这仍然是你应该定期查看,并与 upstream data source owners 沟通的内容。我们太常看到 data teams 在 SQL models 中实现奇怪 logic,试图绕开 upstream 产生的 data quality issues。如果测试 data sources,就可以更早被 issues alert,并与 data producers 沟通,尝试找到真正的解决方案。
Summary
本章中,我们讨论了 testing 在构建 data warehouse 中的重要性,以及 dbt 如何出色地提供 testing capabilities。我们强调了只依赖 manual checks 的风险,并强调 data projects 中需要 automated 和 comprehensive testing。Testing 可以确保 code robustness、accuracy,以及对不断演进的 data sources 和 schemas 的 adaptability,从而增强对 codebase 的信心,并使 data consumers 能够做出 informed decisions。
我们介绍了 dbt 中两种类型的 tests:singular tests 和 generic tests。Singular tests 是简单 SQL queries,用于检查特定 conditions 并返回 failing rows;generic tests 则是 parameterized queries,可以被多个 purposes 复用,提供 flexibility 和 efficiency。我们讨论了 good test 的 attributes,包括 automated、fast、reliable、informative 和 focused。
我们还覆盖了 dbt 提供的 out-of-the-box tests,例如 unique、not_null、accepted_values 和 relationships,它们有助于确保 data accuracy 和 consistency。我们解释了如何在 YAML files 中为 models、sources、seeds 和 snapshots 设置 tests。我们还查看了几个 additional packages,这些 packages 提供大量选项,可以增强你的 testing framework。