了解有关使用dbt实施合同测试的全部内容
让我来告诉你一个关于数据管理系统和规模的故事,如果你是一个试图在2023年尽力工作的数据或分析工程师,它可能会与你产生共鸣。
不久之前,几乎所有的数据架构和数据团队结构都遵循集中化方法。作为一个数据或分析工程师,你知道在哪里找到所有的转换逻辑和模型,因为它们都在同一个代码库中。你可能与构建你正在使用的数据管道的同事密切合作。只有一个数据团队,最多两个。
这种方法对于小型组织和有限的数据源和用例的创业公司是有效的。它也适用于没有完全专注于从数据中提取价值的大型企业。然而,随着组织优先考虑数据驱动,对于更多的机器学习、分析和商业智能数据用例的需求也增加了。
用例和数据源的扩散增加了数据管理的复杂性以及创建和维护数据系统所需的人员数量。为了满足这些需求,你公司数据策略的最新版本可能已经转向分权化。这包括组建分权化的数据团队和采用分权化的数据架构,如数据网格(Data Mesh)。
分权化允许组织扩展数据管理,但带来了确保不同组件之间协调的新挑战,例如由不同团队开发和管理的数据产品和数据管道。
在这种类型的架构和组织结构中,往往不清楚谁对每个组件负责,导致问题和责任转移。团队之间的集成点的数量也增加了,维护不同组件之间的有效接口变得更加困难。
如果你能与这种情况产生共鸣,那么你并不孤单。你的组织可能正在进行数据的分权化。为了应对这种转变,我们可以借鉴分权化和分布式架构(如微服务)在操作世界中的成功实现。他们是如何做到的?他们是如何在那样的规模下提供可靠的系统的?嗯,他们利用了现代化的测试技术。
“多年来,软件工程成功地拥抱了“两个披萨团队”执行小单位工作的概念。每个团队拥有一个更大系统的独立组件。团队通过明确定义的、版本化的接口相互集成。可悲的是,数据却仍然落后于软件。分块数据架构仍然是常态,尽管存在明显的缺点。” — dbt labs
在本文中,我将介绍其中一种技术:合同测试。我将展示你如何使用dbt为上游来源和你的dbt模型的公共接口创建简单的合同测试。随着你的dbt应用程序变得越来越复杂和分散,这种类型的测试将使你保持理智。
但是……什么是合同测试?
当一个分布式系统开始进入由多个团队开发的多个组件的阶段时,团队可能尝试的第一种验证系统是否按预期行为的方法是实施对整个系统进行端到端测试。
由于其复杂性、反馈速度缓慢以及难以维护和编排的困难,端到端测试经常变得非常难以使用。
在大规模实施微服务时,这在操作世界中是个问题。当整个系统不能进行测试时,工程团队开始采用不同的方法,如合同测试。
“集成合同测试是对外部服务边界的测试,以验证其是否符合消费服务预期的合同。” — Toby Clemson
团队仍然可以保留一小部分的端到端测试,但他们通过使用合同、组件和单元测试等更快速和可靠性更高的测试来下降测试金字塔。
不同测试类型的权衡通常可以通过测试金字塔进行可视化。我在我的先前一篇关于对dbt模型进行单元测试的实现的文章中提到了这个概念。
如果我们将相同的概念应用到数据管理系统上,可以实施dbt应用的合同测试来验证两种类型接口的行为:
- 上游数据源。
- 公共接口,如数据仓库和输出接口。
合同测试在数据系统中的优势
正如我们所见,数据架构正如操作服务一样变得更加复杂和分散。随着这种类型的系统继续扩展,运行可维护和有效的端到端测试套件的能力减弱。
合同测试成为在不同情况下管理的强大工具,提供了多个优势:
- 减少验证系统行为所需的端到端测试数量。从而提供更快的反馈和更低的维护成本。
- 通过为团队的公共接口提供明确的期望,来管理在同一个代码库中进行的分离数据团队的复杂性。
- 在达到生产之前,在较低的环境中暴露组件之间的集成问题。
- 更好地定义和记录不同数据流水线或数据产品之间的接口。
合同测试 vs 数据质量测试
您可能会想,但是…合同测试的概念听起来像我们已经在数据流水线中运行的质量测试。
这是个合理的观察,因为合同测试和数据质量测试的范围之间存在模糊的界限。我喜欢将合同测试视为现代数据测试策略的一部分,作为质量测试的子集。
区别在于合同测试关注架构和约束,而数据质量测试关注实际数据及其特征。让我们看一些例子。
合同测试范围:
- 检查列类型。
- 检查模式层面的预期约束,如主键和外键、非空列。
- 检查给定列的接受的值。
- 检查给定列的有效范围。
质量测试范围:
- 评估完整性,例如:在列中非空值的百分比。
- 评估唯一性,例如:不唯一的行数。
- 评估一致性,例如:源中的所有用户标识符都包含在输出中。
实施我们的第一个合同测试
好了,理论够了,让我们通过一个简单的例子开始行动。我们有一个名为health-insights的dbt应用程序,它从上游数据源获取体重和身高数据,并计算身体质量指数。
我们了不起的后端团队的同事负责生成我们构建health-insights应用程序所需的体重和身高数据。他们在另一个有些忙碌和紧张的团队中工作。有时,他们会忘记通知我们模式的更改。为了测试上游接口的这些变化,我们决定创建我们的第一个源合同测试。
首先,我们需要添加两个新的dbt包,dbt-expectations和dbt-utils,这将允许我们对源的模式和接受的值进行断言。
# packages.ymlpackages: - package: dbt-labs/dbt_utils version: 1.1.1 - package: calogica/dbt_expectations version: 0.8.5
测试数据来源
我们先为第一个数据源定义一个合同测试。我们从名为raw_height的表中提取来自健身房应用程序用户的身高信息。
我们与数据生产者达成一致,我们将收到身高测量值、测量单位和用户ID。我们就数据类型达成一致,并且只支持“cm”和“inches”作为单位,有了这些,我们可以在dbt源YAML文件中定义我们的第一个合同。
构建模块
从前面的测试中可以看到,我们使用了几个dbt-unit-testing宏:
- dbt_expectations.expect_column_values_to_be_of_type: 这个断言允许我们定义期望的列数据类型。
- accepted_values: 这个断言允许我们为特定列定义一组接受的值。
- dbt_utils.accepted_range: 这个断言允许我们为给定列定义一个数字范围。在这个例子中,我们期望列的值不小于0。
- not null: 最后,内置的断言如“not null”允许我们定义列约束。
使用这些构建模块,我们添加了几个测试来定义上述合同期望。还请注意我们将测试标记为“contract-test-source”。这个标记允许我们独立运行所有的合同测试,无论是本地运行,还是后面将要看到的CI/CD流水线:
dbt test --select tag:contract-test-source
为Marts和输出端口实施合同测试
我们已经看到了如何快速为我们的dbt应用程序的源创建合同测试,那我们的数据管道或数据产品的公共接口呢?
作为数据生产者,我们希望确保我们按照数据消费者的期望产生数据,以便满足我们与他们的合同,并使我们的数据管道或数据产品可信任和可靠。
确保我们履行对数据消费者的义务的一个简单方法是为我们的公共接口添加合同测试。
Dbt 最近发布了一个新功能,用于SQL模型,即模型合同,允许定义dbt模型的合同。在构建模型时,dbt将验证您的模型转换是否产生与其合同匹配的数据集,否则将无法构建。
让我们看看它的实际应用。我们的市场,body_mass_indexes,从我们获取的重量和身高测量数据中生成一个BMI指标。与我们的提供商签订的合同规定如下:
- 每个列的数据类型。
- 用户ID不能为空。
- 用户ID始终大于0。
让我们使用dbt模型合同来定义body_mass_indexes模型的合同:
构建模块
通过查看先前的模型规范文件,我们可以看到几个元数据,可以帮助我们定义合同。
- contract.enforced:此配置告诉dbt每次运行模型时都要强制执行合同。
- data_type:此断言允许我们定义模型运行后预期的列类型。
- constraints:最后,约束块为我们提供了定义有用约束的机会,例如列不能为null,设置主键以及自定义表达式。在上面的示例中,我们定义了一个约束,告诉dbt用户ID必须始终大于0。您可以在此处查看所有可用的约束。
我们为数据源定义的合同测试和为我们的市场或输出端口定义的测试之间的一个差异是合同何时进行验证和强制执行。
当通过dbt run生成模型时,模型合同受到强制执行,而基于dbt测试的合同在运行dbt测试时受到强制执行。
如果其中一个模型合同未满足,当您执行“dbt run”时,您将看到一个错误,其中包含有关失败的详细信息。以下是dbt run控制台输出的示例。
1 of 4 START sql table model dbt_testing_example.stg_gym_app__height ........... [RUN]2 of 4 START sql table model dbt_testing_example.stg_gym_app__weight ........... [RUN]2 of 4 OK created sql table model dbt_testing_example.stg_gym_app__weight ...... [SELECT 4 in 0.88s]1 of 4 OK created sql table model dbt_testing_example.stg_gym_app__height ...... [SELECT 4 in 0.92s]3 of 4 START sql table model dbt_testing_example.int_weight_measurements_with_latest_height [RUN]3 of 4 OK created sql table model dbt_testing_example.int_weight_measurements_with_latest_height [SELECT 4 in 0.96s]4 of 4 START sql table model dbt_testing_example.body_mass_indexes ............. [RUN]4 of 4 ERROR creating sql table model dbt_testing_example.body_mass_indexes .... [ERROR in 0.77s]Finished running 4 table models in 0 hours 0 minutes and 6.28 seconds (6.28s).Completed with 1 error and 0 warnings:Database Error in model body_mass_indexes (models/marts/body_mass_indexes.sql) new row for relation "body_mass_indexes__dbt_tmp" violates check constraint "body_mass_indexes__dbt_tmp_user_id_check1" DETAIL: Failing row contains (1, 2009-07-01, 82.5, null, null). compiled Code at target/run/dbt_testing_example/models/marts/body_mass_indexes.sql
在流水线中运行合同测试
到目前为止,我们有一个功能强大的合同测试套件,但我们如何以及何时运行它们呢?
我们可以在两种类型的流水线中运行合同测试。
- CI/CD流水线
- 数据流水线
例如,您可以在CI/CD流水线中以时间表执行源合同测试,该流水线针对测试或分期等较低环境中可用的数据源。您可以设置流水线,以便每当合同未达到时失败。
这些失败提供了有关其他团队在这些更改到达生产之前引入的违反合同的更改的有价值的信息。
您还可以通过CI/CD流水线在部署新更改时运行输出端口/商场合同测试。由于每次构建模型时都会检查dbt模型合同,您可以告诉dbt强制执行合同,以便如果新模型的更改在合同中引入了破坏性变化,您的团队将在您的数据消费者受到影响之前收到通知。
最后,您还可以在生产中的数据流水线中运行源代码和输出端口/商场合同测试。在生产中运行合同测试可以帮助您的团队了解数据流水线失败是因为上游依赖关系违反了合同还是因为您生成的数据未满足与下游消费者的合同。
开始的其他提示
- 从小的地方开始,测试那些更脆弱易于失败的集成点。
- 在实施合同测试时应用宽容的读者模式。只断言你需要的数据。
- 根据您的需求调整合同测试的行为,可以配置严重性属性以使其大声失败或仅发出警告。
- 将这些类型的测试与像Montecarlo这样的现代数据可观测性工具集成,使其成为您的事件管理流程的一部分。
- 即使您的数据系统不是使用dbt开发的,也可以利用dbt的合同测试。您仍然可以在dbt中定义源代码合同测试,并针对使用其他框架或纯SQL创建的表格或文件执行这些测试。
- 考虑使用更高级的合同测试技术,如消费者驱动的合同,可以更轻松地在特定上下文中实施合同测试。
结论
我们已经看到了随着数据系统变得越来越分散和复杂,测试数据系统的策略也可以从合同测试等测试技术中受益。
我们还看到了如何借助dbt内置功能和其他dbt软件包开始实施合同测试。我们将这些类型的测试应用于两个集成点:上游数据源和数据商场/输出端口。
我希望本文能为您和您的团队提供所有工具和提示,以便在数据系统扩展以满足新的数据用例时开始实施合同测试。如果您感兴趣,您可以在此Github仓库中查看示例dbt应用程序的源代码。
您准备好开始尝试并启动您的合同测试之旅了吗?我很想听听您的想法和经验,请在评论中告诉我。
在我下一篇文章中,我将讨论我的测试数据系统系列中缺失的部分,即数据质量测试以及如何在dbt中实施这些测试。
感谢我Thoughtworks的同事Arne和Manisha抽出时间审查本文的早期版本。感谢dbt-expectations软件包的维护者们的出色工作。
除非另有说明,所有图片均为作者提供。