Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable macro unit tests #79

Merged
merged 4 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,38 @@ _NOTE: currently only the MERGE strategy is supported, so `unit_test_incremental
this: ref('dmt__current_state_orders_2')
expected_output: ref('dmt__expected_stg_orders_2')
```

### Set the unit tests as macros

The unit tests can also be defined inside macros. This yields the disadvantage that not all the tests are defined at the same place, i.e. the yml file of the model. However, this allows to easlily run a specific unit test and enables easier selection criterias if the tests are for example run within a ci/cd pipeline, since all the tests can be excluded or included via their folder path within tests/.

To set a new test, a file has to be created within the tests/ folder like that:

```sql
{{ dbt_datamocktool.unit_test(
model = ref('stg_customers'),
input_mapping = {
source('jaffle_shop', 'raw_customers'): ref('dmt__raw_customers_1')
},
expected_output = ref('dmt__expected_stg_customers_1'),
) }}
```

To make use of the other configuration possibilities, like inlcuding only specific columns, they can be simply added the same then in the yml files. If a specification consists out of multiple items, it has to explicitly be setup as a dictionary. Does one key contain multiple calues, it has to be added as a list. A complete example would look like that:

```sql
{{ dbt_datamocktool.unit_test(
model = ref('<model_to_test>'),
input_mapping = {
ref('<input_one>'): ref('<replacement_one>'),
ref('<input_two>'): ref('<replacement_two>')
},
expected_output = ref('<expected_output>'),
name = '<Name of the unit test>',
description = '<Description of the unit test>',
compare_columns = ['<col_one>', '<col_two>'],
depends_on = [ref('<dependency_one>'), ref('<dependency_two>')],
)}}
```

Up to this moment, not multiple tests can defined per file. It can be only one test macro per file.
8 changes: 8 additions & 0 deletions integration_tests/tests/unit/staging/test_stg_customers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{#- Unit test with seeds as the input as well as the output. -#}
{{ dbt_datamocktool.unit_test(
model = ref('stg_customers'),
input_mapping = {
source('jaffle_shop', 'raw_customers'): ref('dmt__raw_customers_1')
},
expected_output = ref('dmt__expected_stg_customers_1'),
) }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{#- Unit test with macro as the input and a model as the expected output. -#}
{{ dbt_datamocktool.unit_test(
model = ref('stg_customers'),
input_mapping = {
source('jaffle_shop', 'raw_customers'): "{{ dmt_raw_customers() }}"
},
expected_output = ref('dmt__expected_stg_customers_2'),
name = "This test is a unit test",
) }}
7 changes: 7 additions & 0 deletions integration_tests/tests/unit/staging/test_stg_orders.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{ dbt_datamocktool.unit_test(
model = ref('stg_orders'),
input_mapping = {
ref('raw_orders'): ref('dmt__raw_orders_1')
},
expected_output = ref('dmt__expected_stg_orders_1'),
) }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{#- Unit test with for an incremental model with redefining `{{this}}`. -#}
{{ dbt_datamocktool.unit_test_incremental(
model = ref('stg_orders'),
input_mapping = {
ref('raw_orders'): ref('dmt__raw_orders_3'),
"this": ref('dmt__current_state_orders_2')
},
expected_output = ref('dmt__expected_stg_orders_2'),
) }}
61 changes: 36 additions & 25 deletions macros/dmt_get_test_sql.sql
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{% macro get_unit_test_sql(model, input_mapping, depends_on) %}
{% set ns=namespace(
test_sql="(select 1) raw_code",
rendered_keys={},
rendered_mappings={},
graph_model=none
) %}

{% do dbt_datamocktool.__set_rendered_keys(ns, input_mapping.keys()) %}
{% do dbt_datamocktool.__set_rendered_mappings(ns, input_mapping) %}

{% if execute %}
{# inside an execute block because graph nodes aren't well-defined during parsing #}
{% set ns.graph_model = dbt_datamocktool.__get_graph_model(project_name, model.schema, model.name) %}
{% set ns.test_sql = ns.graph_model.raw_code %}

{% do dbt_datamocktool.__render_sql_and_replace_references(ns, input_mapping) %}

{% set mock_model_relation = dbt_datamocktool._get_model_to_mock(
model, suffix=('_dmt_' ~ modules.datetime.datetime.now().strftime("%S%f"))
) %}
{% do dbt_datamocktool.__render_sql_and_replace_references(ns, input_mapping) %}
{% set mock_model_relation = dbt_datamocktool._get_model_to_mock(
model, suffix=('_dmt_' ~ modules.datetime.datetime.now().strftime("%S%f"))
) %}

{% do dbt_datamocktool._create_mock_table_or_view(mock_model_relation, ns.test_sql) %}

Expand All @@ -25,36 +25,36 @@
{% for k in depends_on %}
-- depends_on: {{ k }}
{% endfor %}

{{ mock_model_relation }}
{% endmacro %}

{% macro get_unit_test_incremental_sql(model, input_mapping, depends_on) %}
{% set ns=namespace(
test_sql="(select 1) raw_code",
rendered_keys={},
rendered_mappings={},
graph_model=none
) %}

{# doing this outside the execute block allows dbt to infer the proper dependencies #}
{% do dbt_datamocktool.__set_rendered_keys(ns, input_mapping.keys()) %}

{% do dbt_datamocktool.__set_rendered_mappings(ns, input_mapping) %}
{% if execute %}
{# inside an execute block because graph nodes aren't well-defined during parsing #}
{% set ns.graph_model = dbt_datamocktool.__get_graph_model(project_name, model.schema, model.name) %}

{% set ns.test_sql = ns.graph_model.raw_code %}

{# replace is_incremental blocks to true to enable incremental code #}
{% set ns.test_sql = ns.test_sql|replace('is_incremental()','true') %}
{% set ns.test_sql = ns.test_sql|replace('is_incremental()','true') %}

{% do dbt_datamocktool.__render_sql_and_replace_references(ns, input_mapping) %}

{# after rendering - replace "this" with mock project and model #}
{# TODO: try catch -- if this not exists in input mapping #}
{% set ns.test_sql = ns.test_sql|replace(this.dataset, model.dataset) %}
{% set ns.test_sql = ns.test_sql|replace(this.table, input_mapping.this) %}
{% set ns.test_sql = ns.test_sql|replace(this.dataset, model.dataset) %}
{% set ns.test_sql = ns.test_sql|replace(this.table, input_mapping.this) %}

{# mock_model_relation is the mocked model name #}
{% set mock_model_relation = dbt_datamocktool._get_model_to_mock(
model, suffix=('_dmt_' ~ modules.datetime.datetime.now().strftime("%S%f"))
Expand Down Expand Up @@ -123,13 +123,13 @@
{% do run_query(get_merge_sql(model, test_sql, dest_columns=dest_columns)) %}
{% endmacro %}

{% macro __set_rendered_keys(ns, keys) %}
{% for k in keys %}
{% do ns.rendered_keys.update({k: render("{{ " + k + " }}")}) %}
{% macro __set_rendered_mappings(ns, input_mapping) %}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This forces the ref in the sql files and the yml files to be EXACTLY the same

{% for k, v in input_mapping.items() %}
{% do ns.rendered_mappings.update({k: render(v)}) %}
{% endfor %}
{% endmacro %}

{% macro __get_graph_model(project_name, model_schema, model_name) %}
{% macro __get_graph_model(project_name, model_schema, model_name) %}
{% set graph_model = graph.nodes.get("model." + project_name + "." + model_name) %}
{# if the model uses an alias, the above call was unsuccessful, so loop through the graph to grab it by the alias instead #}
{% if graph_model is none %}
Expand All @@ -143,9 +143,20 @@
{{ return(graph_model) }}
{% endmacro %}

{% macro __render_sql_and_replace_references(ns, input_mapping) %}
{% for k,v in input_mapping.items() %}
{# render the original sql and replacement key before replacing because v is already rendered when it is passed to this test #}
{% set ns.test_sql = render(ns.test_sql)|replace(ns.rendered_keys[k], v) %}
{% macro __render_sql_and_replace_references(ns, input_mapping) %}
{#- Replace the keys first, before the sql code is rendered -#}
{% for k, v in ns.rendered_mappings.items() %}
{% set ns.test_sql = ns.test_sql|replace("{{ "~render(k)~" }}", v) %}
{% endfor %}

{#- Render the original sql after all reference values are set according to the provided input
mapping. -#}
{% set ns.test_sql = render(ns.test_sql) %}

{#- Replace left over rendered keys with their reference values. This is only necessary, if the
unit test is defined within a macro, since then the input mapping is already rendered within
the macro itself.-#}
{% for k, v in ns.rendered_mappings.items() %}
{% set ns.test_sql = ns.test_sql|replace(k, v) %}
{% endfor %}
{% endmacro %}
15 changes: 12 additions & 3 deletions macros/dmt_unit_test.sql
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
{%- test unit_test(model, input_mapping, expected_output, name, description, compare_columns, depends_on) -%}
{%- macro unit_test(model, input_mapping, expected_output, name, description, compare_columns, depends_on) -%}
{%- set test_sql = dbt_datamocktool.get_unit_test_sql(model, input_mapping, depends_on)|trim -%}
{%- set test_report = dbt_datamocktool.test_equality(expected_output, name, compare_model=test_sql, compare_columns=compare_columns) -%}
{{ test_report }}
{%- endmacro -%}

{%- test unit_test(model, input_mapping, expected_output, name, description, compare_columns, depends_on) -%}
{{ dbt_datamocktool.unit_test(model, input_mapping, expected_output, name, description, compare_columns, depends_on) }}
{%- endtest -%}

{% test unit_test_incremental(model, input_mapping, expected_output, name, description, compare_columns, depends_on) %}

{%- macro unit_test_incremental(model, input_mapping, expected_output, name, description, compare_columns, depends_on) -%}
{%- set test_sql = dbt_datamocktool.get_unit_test_incremental_sql(model, input_mapping, depends_on)|trim -%}
{%- set test_report = dbt_datamocktool.test_equality(expected_output, name, compare_model=test_sql, compare_columns=compare_columns) -%}
{{ test_report }}
{%- endmacro -%}

{% test unit_test_incremental(model, input_mapping, expected_output, name, description, compare_columns, depends_on) %}
{{ dbt_datamocktool.unit_test_incremental(model, input_mapping, expected_output, name, description, compare_columns, depends_on) }}
{% endtest %}

{%- macro test_equality(model, name, compare_model, compare_columns=None) -%}

{%- macro test_equality(model, name, compare_model, compare_columns=None) -%}
{#-- Prevent querying of db in parsing mode. This works because this macro does not create any new refs. #}
{%- if not execute -%}
{{ return('') }}
Expand Down