Skip to content

Commit b5a0c4c

Browse files
gshankMichelleArkemmyoopjtcohen6aranke
authored
Unit testing feature branch pull request (#8411)
* Initial implementation of unit testing (from pr #2911) Co-authored-by: Michelle Ark <[email protected]> * 8295 unit testing artifacts (#8477) * unit test config: tags & meta (#8565) * Add additional functional test for unit testing selection, artifacts, etc (#8639) * Enable inline csv format in unit testing (#8743) * Support unit testing incremental models (#8891) * update unit test key: unit -> unit-tests (#8988) * convert to use unit test name at top level key (#8966) * csv file fixtures (#9044) * Unit test support for `state:modified` and `--defer` (#9032) Co-authored-by: Michelle Ark <[email protected]> * Allow use of sources as unit testing inputs (#9059) * Use daff for diff formatting in unit testing (#8984) * Fix #8652: Use seed file from disk for unit testing if rows not specified in YAML config (#9064) Co-authored-by: Michelle Ark <[email protected]> Fix #8652: Use seed value if rows not specified * Move unit testing to test and build commands (#9108) * Enable unit testing in non-root packages (#9184) * convert test to data_test (#9201) * Make fixtures files full-fledged members of manifest and enable partial parsing (#9225) * In build command run unit tests before models (#9273) --------- Co-authored-by: Michelle Ark <[email protected]> Co-authored-by: Michelle Ark <[email protected]> Co-authored-by: Emily Rockman <[email protected]> Co-authored-by: Jeremy Cohen <[email protected]> Co-authored-by: Kshitij Aranke <[email protected]>
1 parent 15704ab commit b5a0c4c

File tree

167 files changed

+12171
-4083
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

167 files changed

+12171
-4083
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Initial implementation of unit testing
3+
time: 2023-08-02T14:50:11.391992-04:00
4+
custom:
5+
Author: gshank
6+
Issue: "8287"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Unit test manifest artifacts and selection
3+
time: 2023-08-28T10:18:25.958929-04:00
4+
custom:
5+
Author: gshank
6+
Issue: "8295"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Support config with tags & meta for unit tests
3+
time: 2023-09-06T23:47:41.059915-04:00
4+
custom:
5+
Author: michelleark
6+
Issue: "8294"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Enable inline csv fixtures in unit tests
3+
time: 2023-09-28T16:32:05.573776-04:00
4+
custom:
5+
Author: gshank
6+
Issue: "8626"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Support unit testing incremental models
3+
time: 2023-11-01T10:18:45.341781-04:00
4+
custom:
5+
Author: michelleark
6+
Issue: "8422"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Add support of csv file fixtures to unit testing
3+
time: 2023-11-06T19:47:52.501495-06:00
4+
custom:
5+
Author: emmyoop
6+
Issue: "8290"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Unit tests support --defer and state:modified
3+
time: 2023-11-07T23:10:06.376588-05:00
4+
custom:
5+
Author: jtcohen6
6+
Issue: "8517"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Support source inputs in unit tests
3+
time: 2023-11-11T19:11:50.870494-05:00
4+
custom:
5+
Author: gshank
6+
Issue: "8507"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Use daff to render diff displayed in stdout when unit test fails
3+
time: 2023-11-14T10:15:55.689307-05:00
4+
custom:
5+
Author: michelleark
6+
Issue: "8558"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Move unit testing to test command
3+
time: 2023-11-16T14:40:06.121336-05:00
4+
custom:
5+
Author: gshank
6+
Issue: "8979"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Support unit tests in non-root packages
3+
time: 2023-11-30T13:09:48.206007-05:00
4+
custom:
5+
Author: gshank
6+
Issue: "8285"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: Features
2+
body: Convert the `tests` config to `data_tests` in both dbt_project.yml and schema files.
3+
in schema files.
4+
time: 2023-12-05T13:17:17.647765-06:00
5+
custom:
6+
Author: emmyoop
7+
Issue: "8699"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Make fixture files full-fledged parts of the manifest and enable partial parsing
3+
time: 2023-12-05T20:04:47.117029-05:00
4+
custom:
5+
Author: gshank
6+
Issue: "9067"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: In build command run unit tests before models
3+
time: 2023-12-12T15:05:56.778829-05:00
4+
custom:
5+
Author: gshank
6+
Issue: "9128"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: Use seed file from disk for unit testing if rows not specified in YAML config
3+
time: 2023-11-13T15:45:35.008565Z
4+
custom:
5+
Author: aranke
6+
Issue: "8652"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Under the Hood
2+
body: Add unit testing functional tests
3+
time: 2023-09-12T19:05:06.023126-04:00
4+
custom:
5+
Author: gshank
6+
Issue: "8512"

core/dbt/adapters/base/relation.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def add_ephemeral_prefix(name: str):
214214
def create_ephemeral_from(
215215
cls: Type[Self],
216216
relation_config: RelationConfig,
217-
limit: Optional[int],
217+
limit: Optional[int] = None,
218218
) -> Self:
219219
# Note that ephemeral models are based on the name.
220220
identifier = cls.add_ephemeral_prefix(relation_config.name)

core/dbt/adapters/events/adapter_types_pb2.py

+185-189
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/dbt/adapters/include/global_project/macros/materializations/tests/helpers.sql

+28
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,31 @@
1212
{{ "limit " ~ limit if limit != none }}
1313
) dbt_internal_test
1414
{%- endmacro %}
15+
16+
17+
{% macro get_unit_test_sql(main_sql, expected_fixture_sql, expected_column_names) -%}
18+
{{ adapter.dispatch('get_unit_test_sql', 'dbt')(main_sql, expected_fixture_sql, expected_column_names) }}
19+
{%- endmacro %}
20+
21+
{% macro default__get_unit_test_sql(main_sql, expected_fixture_sql, expected_column_names) -%}
22+
-- Build actual result given inputs
23+
with dbt_internal_unit_test_actual AS (
24+
select
25+
{% for expected_column_name in expected_column_names %}{{expected_column_name}}{% if not loop.last -%},{% endif %}{%- endfor -%}, {{ dbt.string_literal("actual") }} as actual_or_expected
26+
from (
27+
{{ main_sql }}
28+
) _dbt_internal_unit_test_actual
29+
),
30+
-- Build expected result
31+
dbt_internal_unit_test_expected AS (
32+
select
33+
{% for expected_column_name in expected_column_names %}{{expected_column_name}}{% if not loop.last -%}, {% endif %}{%- endfor -%}, {{ dbt.string_literal("expected") }} as actual_or_expected
34+
from (
35+
{{ expected_fixture_sql }}
36+
) _dbt_internal_unit_test_expected
37+
)
38+
-- Union actual and expected results
39+
select * from dbt_internal_unit_test_actual
40+
union all
41+
select * from dbt_internal_unit_test_expected
42+
{%- endmacro %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{%- materialization unit, default -%}
2+
3+
{% set relations = [] %}
4+
5+
{% set expected_rows = config.get('expected_rows') %}
6+
{% set tested_expected_column_names = expected_rows[0].keys() if (expected_rows | length ) > 0 else get_columns_in_query(sql) %} %}
7+
8+
{%- set target_relation = this.incorporate(type='table') -%}
9+
{%- set temp_relation = make_temp_relation(target_relation)-%}
10+
{% do run_query(get_create_table_as_sql(True, temp_relation, get_empty_subquery_sql(sql))) %}
11+
{%- set columns_in_relation = adapter.get_columns_in_relation(temp_relation) -%}
12+
{%- set column_name_to_data_types = {} -%}
13+
{%- for column in columns_in_relation -%}
14+
{%- do column_name_to_data_types.update({column.name: column.dtype}) -%}
15+
{%- endfor -%}
16+
17+
{% set unit_test_sql = get_unit_test_sql(sql, get_expected_sql(expected_rows, column_name_to_data_types), tested_expected_column_names) %}
18+
19+
{% call statement('main', fetch_result=True) -%}
20+
21+
{{ unit_test_sql }}
22+
23+
{%- endcall %}
24+
25+
{% do adapter.drop_relation(temp_relation) %}
26+
27+
{{ return({'relations': relations}) }}
28+
29+
{%- endmaterialization -%}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{% macro get_fixture_sql(rows, column_name_to_data_types) %}
2+
-- Fixture for {{ model.name }}
3+
{% set default_row = {} %}
4+
5+
{%- if not column_name_to_data_types -%}
6+
{%- set columns_in_relation = adapter.get_columns_in_relation(this) -%}
7+
{%- set column_name_to_data_types = {} -%}
8+
{%- for column in columns_in_relation -%}
9+
{%- do column_name_to_data_types.update({column.name: column.dtype}) -%}
10+
{%- endfor -%}
11+
{%- endif -%}
12+
13+
{%- if not column_name_to_data_types -%}
14+
{{ exceptions.raise_compiler_error("Not able to get columns for unit test '" ~ model.name ~ "' from relation " ~ this) }}
15+
{%- endif -%}
16+
17+
{%- for column_name, column_type in column_name_to_data_types.items() -%}
18+
{%- do default_row.update({column_name: (safe_cast("null", column_type) | trim )}) -%}
19+
{%- endfor -%}
20+
21+
{%- for row in rows -%}
22+
{%- do format_row(row, column_name_to_data_types) -%}
23+
{%- set default_row_copy = default_row.copy() -%}
24+
{%- do default_row_copy.update(row) -%}
25+
select
26+
{%- for column_name, column_value in default_row_copy.items() %} {{ column_value }} AS {{ column_name }}{% if not loop.last -%}, {%- endif %}
27+
{%- endfor %}
28+
{%- if not loop.last %}
29+
union all
30+
{% endif %}
31+
{%- endfor -%}
32+
33+
{%- if (rows | length) == 0 -%}
34+
select
35+
{%- for column_name, column_value in default_row.items() %} {{ column_value }} AS {{ column_name }}{% if not loop.last -%},{%- endif %}
36+
{%- endfor %}
37+
limit 0
38+
{%- endif -%}
39+
{% endmacro %}
40+
41+
42+
{% macro get_expected_sql(rows, column_name_to_data_types) %}
43+
44+
{%- if (rows | length) == 0 -%}
45+
select * FROM dbt_internal_unit_test_actual
46+
limit 0
47+
{%- else -%}
48+
{%- for row in rows -%}
49+
{%- do format_row(row, column_name_to_data_types) -%}
50+
select
51+
{%- for column_name, column_value in row.items() %} {{ column_value }} AS {{ column_name }}{% if not loop.last -%}, {%- endif %}
52+
{%- endfor %}
53+
{%- if not loop.last %}
54+
union all
55+
{% endif %}
56+
{%- endfor -%}
57+
{%- endif -%}
58+
59+
{% endmacro %}
60+
61+
{%- macro format_row(row, column_name_to_data_types) -%}
62+
63+
{#-- wrap yaml strings in quotes, apply cast --#}
64+
{%- for column_name, column_value in row.items() -%}
65+
{% set row_update = {column_name: column_value} %}
66+
{%- if column_value is string -%}
67+
{%- set row_update = {column_name: safe_cast(dbt.string_literal(column_value), column_name_to_data_types[column_name]) } -%}
68+
{%- elif column_value is none -%}
69+
{%- set row_update = {column_name: safe_cast('null', column_name_to_data_types[column_name]) } -%}
70+
{%- else -%}
71+
{%- set row_update = {column_name: safe_cast(column_value, column_name_to_data_types[column_name]) } -%}
72+
{%- endif -%}
73+
{%- do row.update(row_update) -%}
74+
{%- endfor -%}
75+
76+
{%- endmacro -%}

core/dbt/adapters/relation_configs/config_change.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class RelationConfigChangeAction(StrEnum):
1212
drop = "drop"
1313

1414

15-
@dataclass(frozen=True, eq=True, unsafe_hash=True)
15+
@dataclass(frozen=True, eq=True, unsafe_hash=True) # type: ignore
1616
class RelationConfigChange(RelationConfigBase, ABC):
1717
action: RelationConfigChangeAction
1818
context: Hashable # this is usually a RelationConfig, e.g. IndexConfig, but shouldn't be limited

core/dbt/clients/jinja.py

+20
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ def __call__(self, *args, **kwargs):
8484
return self.call_macro(*args, **kwargs)
8585

8686

87+
class UnitTestMacroGenerator(MacroGenerator):
88+
# this makes UnitTestMacroGenerator objects callable like functions
89+
def __init__(
90+
self,
91+
macro_generator: MacroGenerator,
92+
call_return_value: Any,
93+
) -> None:
94+
super().__init__(
95+
macro_generator.macro,
96+
macro_generator.context,
97+
macro_generator.node,
98+
macro_generator.stack,
99+
)
100+
self.call_return_value = call_return_value
101+
102+
def __call__(self, *args, **kwargs):
103+
with self.track_call():
104+
return self.call_return_value
105+
106+
87107
# performance note: Local benmcharking (so take it with a big grain of salt!)
88108
# on this indicates that it is is on average slightly slower than
89109
# checking two separate patterns, but the standard deviation is smaller with

core/dbt/compilation.py

+21-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
from dbt.flags import get_flags
1212
from dbt.adapters.factory import get_adapter
1313
from dbt.clients import jinja
14+
from dbt.context.providers import (
15+
generate_runtime_model_context,
16+
generate_runtime_unit_test_context,
17+
)
1418
from dbt_common.clients.system import make_directory
15-
from dbt.context.providers import generate_runtime_model_context
1619
from dbt.contracts.graph.manifest import Manifest, UniqueID
1720
from dbt.contracts.graph.nodes import (
1821
ManifestNode,
@@ -21,6 +24,8 @@
2124
GraphMemberNode,
2225
InjectedCTE,
2326
SeedNode,
27+
UnitTestNode,
28+
UnitTestDefinition,
2429
)
2530
from dbt.exceptions import (
2631
GraphDependencyNotFoundError,
@@ -43,7 +48,8 @@
4348
def print_compile_stats(stats):
4449
names = {
4550
NodeType.Model: "model",
46-
NodeType.Test: "test",
51+
NodeType.Test: "data test",
52+
NodeType.Unit: "unit test",
4753
NodeType.Snapshot: "snapshot",
4854
NodeType.Analysis: "analysis",
4955
NodeType.Macro: "macro",
@@ -91,6 +97,7 @@ def _generate_stats(manifest: Manifest):
9197
stats[NodeType.Macro] += len(manifest.macros)
9298
stats[NodeType.Group] += len(manifest.groups)
9399
stats[NodeType.SemanticModel] += len(manifest.semantic_models)
100+
stats[NodeType.Unit] += len(manifest.unit_tests)
94101

95102
# TODO: should we be counting dimensions + entities?
96103

@@ -128,7 +135,7 @@ class Linker:
128135
def __init__(self, data=None) -> None:
129136
if data is None:
130137
data = {}
131-
self.graph = nx.DiGraph(**data)
138+
self.graph: nx.DiGraph = nx.DiGraph(**data)
132139

133140
def edges(self):
134141
return self.graph.edges()
@@ -191,6 +198,8 @@ def link_graph(self, manifest: Manifest):
191198
self.link_node(exposure, manifest)
192199
for metric in manifest.metrics.values():
193200
self.link_node(metric, manifest)
201+
for unit_test in manifest.unit_tests.values():
202+
self.link_node(unit_test, manifest)
194203
for saved_query in manifest.saved_queries.values():
195204
self.link_node(saved_query, manifest)
196205

@@ -234,6 +243,7 @@ def add_test_edges(self, manifest: Manifest) -> None:
234243
# Get all tests that depend on any upstream nodes.
235244
upstream_tests = []
236245
for upstream_node in upstream_nodes:
246+
# This gets tests with unique_ids starting with "test."
237247
upstream_tests += _get_tests_for_node(manifest, upstream_node)
238248

239249
for upstream_test in upstream_tests:
@@ -291,8 +301,10 @@ def _create_node_context(
291301
manifest: Manifest,
292302
extra_context: Dict[str, Any],
293303
) -> Dict[str, Any]:
294-
295-
context = generate_runtime_model_context(node, self.config, manifest)
304+
if isinstance(node, UnitTestNode):
305+
context = generate_runtime_unit_test_context(node, self.config, manifest)
306+
else:
307+
context = generate_runtime_model_context(node, self.config, manifest)
296308
context.update(extra_context)
297309

298310
if isinstance(node, GenericTestNode):
@@ -460,6 +472,7 @@ def compile(self, manifest: Manifest, write=True, add_test_edges=False) -> Graph
460472
summaries["_invocation_id"] = get_invocation_id()
461473
summaries["linked"] = linker.get_graph_summary(manifest)
462474

475+
# This is only called for the "build" command
463476
if add_test_edges:
464477
manifest.build_parent_and_child_maps()
465478
linker.add_test_edges(manifest)
@@ -526,6 +539,9 @@ def compile_node(
526539
the node's raw_code into compiled_code, and then calls the
527540
recursive method to "prepend" the ctes.
528541
"""
542+
if isinstance(node, UnitTestDefinition):
543+
return node
544+
529545
# Make sure Lexer for sqlparse 0.4.4 is initialized
530546
from sqlparse.lexer import Lexer # type: ignore
531547

0 commit comments

Comments
 (0)