diff --git a/README.md b/README.md index 3d04313..66919ac 100644 --- a/README.md +++ b/README.md @@ -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(''), + input_mapping = { + ref(''): ref(''), + ref(''): ref('') + }, + expected_output = ref(''), + name = '', + description = '', + compare_columns = ['', ''], + depends_on = [ref(''), ref('')], +)}} +``` + +Up to this moment, not multiple tests can defined per file. It can be only one test macro per file. \ No newline at end of file diff --git a/integration_tests/tests/unit/staging/test_stg_customers.sql b/integration_tests/tests/unit/staging/test_stg_customers.sql new file mode 100644 index 0000000..ffe7e9f --- /dev/null +++ b/integration_tests/tests/unit/staging/test_stg_customers.sql @@ -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'), +) }} diff --git a/integration_tests/tests/unit/staging/test_stg_customers_macro_input.sql b/integration_tests/tests/unit/staging/test_stg_customers_macro_input.sql new file mode 100644 index 0000000..2b563ea --- /dev/null +++ b/integration_tests/tests/unit/staging/test_stg_customers_macro_input.sql @@ -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", +) }} diff --git a/integration_tests/tests/unit/staging/test_stg_orders.sql b/integration_tests/tests/unit/staging/test_stg_orders.sql new file mode 100644 index 0000000..a5a9015 --- /dev/null +++ b/integration_tests/tests/unit/staging/test_stg_orders.sql @@ -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'), +) }} diff --git a/integration_tests/tests/unit/staging/test_stg_orders_incremental.sql b/integration_tests/tests/unit/staging/test_stg_orders_incremental.sql new file mode 100644 index 0000000..679c6b4 --- /dev/null +++ b/integration_tests/tests/unit/staging/test_stg_orders_incremental.sql @@ -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'), +) }} diff --git a/macros/dmt_get_test_sql.sql b/macros/dmt_get_test_sql.sql index ec8cd39..285057c 100644 --- a/macros/dmt_get_test_sql.sql +++ b/macros/dmt_get_test_sql.sql @@ -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) %} @@ -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")) @@ -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) %} + {% 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 %} @@ -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 %} diff --git a/macros/dmt_unit_test.sql b/macros/dmt_unit_test.sql index 0462d51..f76e788 100644 --- a/macros/dmt_unit_test.sql +++ b/macros/dmt_unit_test.sql @@ -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('') }}