{"id":1984,"date":"2022-12-28T13:35:31","date_gmt":"2022-12-28T11:35:31","guid":{"rendered":"http:\/\/52.91.248.125\/whats-in-the-box\/"},"modified":"2023-02-28T11:54:16","modified_gmt":"2023-02-28T09:54:16","slug":"whats-in-the-box","status":"publish","type":"post","link":"https:\/\/www.orfium.com\/news\/whats-in-the-box\/","title":{"rendered":"What\u2019s in the box?"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"http:\/\/52.91.248.125\/wp-content\/uploads\/2023\/02\/Whats-in-the-box-1024x576.jpg\" alt=\"\" class=\"wp-image-1601\"\/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Black box testing for non-data engineers with DBT<\/h2>\n\n\n\n<p>Black box testing is a software testing method in which the functionalities of software applications are tested without having knowledge of internal code structure, implementation details, and internal paths. Let\u2019s borrow that term and use the same analogy to test our black boxes, meaning our dbt models.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/lh3.googleusercontent.com\/VENiLFhDYYkfpEfbUBaTsOnLIn4zQpTVJpwDly4tBg-rIucFbHgTrg0B4Lxd9PbqbpgLyXJfp1o6VWHGM_Q6chy6NGGR0AB8o6VG6H0WmfyWEi9V35ndhl0fPFsMdAHLJoXq5ixvCy-KWbl-zozBfb6RQMEkwi3u9C4S7UWbggT3Yn7ybHe35rLTm9ti\" alt=\"\"\/><\/figure>\n\n\n\n<p>So, adapting the lexicon from software engineering, we have:<\/p>\n\n\n\n<p><strong><em>Black Box<\/em><\/strong>: the dbt model that plays the role of transformation<\/p>\n\n\n\n<p><strong><em>Input(s)<\/em><\/strong>: the different tables that are used in the query. In dbt we call these <em>sources<\/em> or <em>references<\/em><\/p>\n\n\n\n<p><strong><em>Output<\/em><\/strong>: the table formed after the dbt model has transformed the data<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">DBT and testing<\/h2>\n\n\n\n<p>DBT is a data build tool. We use it, due to its simplicity, to perform transformation in our snowflake data cloud for analytical purposes. Not to overstate the matter, but we love it.<\/p>\n\n\n\n<p><a href=\"https:\/\/www.getdbt.com\/blog\/what-exactly-is-dbt\/\">What, exactly, is dbt?<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/www.getdbt.com\/blog\/building-a-mature-analytics-workflow\/\">Building a Mature Analytics Workflow<\/a><\/p>\n\n\n\n<p>DBT already offers dbt tests, and performing them is a great way to test the data on your tables. By default, the available tests are <strong>unique<\/strong>, <strong>not_null<\/strong>, <strong>accepted_values<\/strong>, <strong>relationship<\/strong>. We can even create custom tests, and there are a variety of extensions out there that stretch dbt functionality with additional tests, such as<a href=\"https:\/\/github.com\/calogica\/dbt-expectations\/tree\/0.1.2\/\"> great expectations<\/a> and<a href=\"https:\/\/github.com\/dbt-labs\/dbt-utils\"> dbt-utils<\/a>. These kinds of tests examine the values of your tables, and they are a great way to identify any critical data quality issues. DBT tests look at the output. However, what we want to do is to test the black box, the transformation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">TDD and Data<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Working with Large Tables<\/strong><\/h3>\n\n\n\n<p>More often than not, the tables that we need to build models upon are huge, and accessing billion of rows and performing transformation upon them takes a long time. A 30 minute transformation might be acceptable when it is a part of a production pipeline, but having to wait for half an hour to develop and test the correctness of your transformation is, well, less than ideal.&nbsp;<\/p>\n\n\n\n<p>Of course you are going to run it against the table, but minimizing the number of runs makes everyone happy. This also limits your Snowflake Warehouse Usage which can save cost and make accountants happy as well.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Edge cases not covered in actual data<\/strong><\/h3>\n\n\n\n<p>Another problem we often face is having a dbt model that works for all intents and purposes for multiple months, only to later discover that there are cases which we didn\u2019t think of. Unsurprisingly, having billions of rows of data means that all the possible scenarios are not at all easy to cover. If only there was a way to test for those cases, as well. The solution we at Orfium use is to generate mock data. They may not be real, but they work well enough to cover our edge cases and future-proof our dbt instances.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Good Tests VS Bad Tests<\/strong><\/h3>\n\n\n\n<p>Writing tests for the sake of writing them is worse than not writing them at all. There, we said it.&nbsp;<\/p>\n\n\n\n<p>Let&#8217;s face it, how many times do we introduce tests on a piece of software, get excited and, thanks to the quick TDD process, we just gleam with self-confidence? Before you know it, we\u2019re writing tests that have no value at all and inventing a fantastic metric called coverage. Coverage is important but not as a single metric. It is only a first indication and should not be used as a goal in itself. <strong>Good tests<\/strong> are the ones that provide value. <strong>Bad tests<\/strong>, on the other hand, only add to the debt and the maintenance. Remember, tests are a means to an end. To what end? Writing robust code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Tests as a requirements gathering tool<\/strong><\/h3>\n\n\n\n<p>How many times have we found ourselves sitting in a room with a stakeholder who provides information about a new report that they need. We start formulating questions, and after some back and forth, sooner or later we are reaching the final requirements of the report. So, happily enough after the meeting, we go to our favorite warehouse, only to discover some flaw in the original request that we didn&#8217;t think of when we did our requirements gathering. Working in an agile environment that\u2019s no issue. We just schedule a follow-up meeting and reach a consensus for the edge cases. Final delivery is reached. However, wouldn&#8217;t it be better if actual cases could be drafted in that first meeting? Business and engineering minds often don\u2019t mesh well, so we can use all the help we can get.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/lh6.googleusercontent.com\/5g0SdwnErGuFy2vfvv0Uo13RcmZfksrWYF-y9Y9s6lJDM1g6sZh1ODF6492kbeSnYFQgs_v4taiB8FzX3alRv06Ud0ODsvoZwoRMn_CZfoMc9N6DN2GkPra2EGIkNm4bPpW7XwhKMxDjhutBmStaVNKaVabEKxasAfSOqRaVsXp1dSB3l6Ahjs67Sfgo\" alt=\"\"\/><\/figure>\n\n\n\n<p>Establishing actual scenarios of how a table could look like and what the result would be, helps a lot in the process of gathering requirements.<\/p>\n\n\n\n<p>Consider the following imaginary scenario:<\/p>\n\n\n\n<p>Stakeholder:<\/p>\n\n\n\n<p>For our table that contains our daily revenue for all the videos, I would like a monthly summary revenue per video for advertisement category.<\/p>\n\n\n\n<p>Engineer (gotcha):<\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1select\n\n2 video_id,\n\n3 year(date_rev) as year,\n\n4 month(date_rev) as month,\n\n5 sum(revenue) revenue\n\n6from\n\n7 fct_videos_rev\n\n8where\n\n9 category = 'advertisement'\n\n10group by&nbsp;\n\n11 video_id,\n\n12 year(date_rev),\n\n13 month(date_rev)\n\n14<\/code><\/pre>\n\n\n\n<p>Stakeholder<\/p>\n\n\n\n<p>I would also like to see how many records the summation was comprised of.<\/p>\n\n\n\n<p>Engineer (gotcha):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1select\n\n2 video_id,\n\n3 year(date_rev) as year,\n\n4 month(date_rev) as month,\n\n5 sum(revenue) revenue,\n\n6 count(*) counts\n\n7from\n\n8 fct_videos_rev\n\n9where\n\n10 category = 'advertisement'\n\n11group by&nbsp;\n\n12 video_id,\n\n13 year(date_rev),\n\n14 month(date_rev)\n\n15<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<p>Stakeholder<\/p>\n\n\n\n<p>That can\u2019t be right. Why so many counts?<\/p>\n\n\n\n<p>Engineer<\/p>\n\n\n\n<p>There are many rows with zero revenues, I see. You don\u2019t want them to count towards your total count, is that right?<\/p>\n\n\n\n<p>Stakeholder<\/p>\n\n\n\n<p>Yes.<\/p>\n\n\n\n<p>Engineer (gotcha):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1select\n\n2 video_id,\n\n3 year(date_rev) as year,\n\n4 month(date_rev) as month,\n\n5 sum(revenue) revenue,\n\n6 count(*) counts\n\n7from\n\n8 fct_videos_rev\n\n9where\n\n10 category = 'advertisement'\n\n11 and revenue &gt; 0\n\n12group by&nbsp;\n\n13 video_id,\n\n14 year(date_rev),\n\n15 month(date_rev)\n\n16<\/code><\/pre>\n\n\n\n<p>Of course, this is an exaggerated example. However, imagine if the same dialog went a different way.<\/p>\n\n\n\n<p>Stakeholder:<\/p>\n\n\n\n<p>For our table that contains our daily revenue for all the videos, I would like a monthly summary on a monthly basis per video for the advertisement category.<\/p>\n\n\n\n<p>Engineer:<\/p>\n\n\n\n<p>If table has the form:<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-regular\"><table><tbody><tr><td>video_id<\/td><td>date_rev<\/td><td>category<\/td><td>revenue<\/td><\/tr><tr><td>video_a<\/td><td>2022-02-12<\/td><td>advertisement<\/td><td>10<\/td><\/tr><tr><td>video_a<\/td><td>2022-02-12<\/td><td>advertisement<\/td><td>0<\/td><\/tr><tr><td>video_a<\/td><td>2022-03-12<\/td><td>subscription<\/td><td>15<\/td><\/tr><tr><td>video_a<\/td><td>2022-03-12<\/td><td>advertisement<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Is the result you want like the following?<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-regular\"><table><tbody><tr><td>video_id<\/td><td>year<\/td><td>month<\/td><td>revenue<\/td><\/tr><tr><td>video_a<\/td><td>2022<\/td><td>02<\/td><td>10<\/td><\/tr><tr><td>video_a<\/td><td>2022<\/td><td>03<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Stakeholder<\/p>\n\n\n\n<p>I would also like to see how many records the summation was comprised of.<\/p>\n\n\n\n<p>So the result you want it to be like:<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-regular\"><table><tbody><tr><td>video_id<\/td><td>year<\/td><td>month<\/td><td>revenue<\/td><td>counts<\/td><\/tr><tr><td>video_a<\/td><td>2022<\/td><td>02<\/td><td>10<\/td><td>2<\/td><\/tr><tr><td>video_a<\/td><td>2022<\/td><td>03<\/td><td>1<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Stakeholder<\/p>\n\n\n\n<p>Why does the first row have 2 counts?<\/p>\n\n\n\n<p>Engineer<\/p>\n\n\n\n<p>There are two with zero revenues, I see. You don\u2019t you want them to count towards your total count, is that right?<\/p>\n\n\n\n<p>Stakeholder<\/p>\n\n\n\n<p>Yes.<\/p>\n\n\n\n<p>Engineer (gotcha):<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-regular\"><table><tbody><tr><td>video_id<\/td><td>year<\/td><td>month<\/td><td>revenue<\/td><td>counts<\/td><\/tr><tr><td>video_a<\/td><td>2022<\/td><td>02<\/td><td>10<\/td><td>1<\/td><\/tr><tr><td>video_a<\/td><td>2022<\/td><td>03<\/td><td>1<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>And all that, without having to write a single line of code. Not that an engineer is afraid to write SQL queries. But really, a lot of time is lost in translating business requirements into SQL queries. They are never <em>that<\/em> simple and they are <em>almost<\/em> <em>never<\/em> correct at first try either.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Tests so software engineers can get onboard in SQL<\/strong><\/h3>\n\n\n\n<p>Orfium is a company which, at the time of writing this post, consists of more than 150 engineers. Only 6 of those are data engineers. That might sound strange, given that we are a data-heavy company dealing with billions of rows of data on a monthly basis. So, a new initiative has emerged called data-mesh. This is a program which we practice on a daily basis and are super proud of. One consequence of data mesh is that there are multiple teams handling their own instance of dbt. But, this will be discussed in detail in another post. Stay tuned!<\/p>\n\n\n\n<p>For the most part, software engineers are not familiar with writing complex SQL queries. That\u2019s not their fault, due to the variety of ORM tools available. However, something that software engineers <em>do<\/em> know how to do very well is to write tests.<\/p>\n\n\n\n<p>In order to bridge that gap, practicing test-driven development on writing SQL is something that can help a lot of engineers to get onboard.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Let the fun begin<\/h3>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/lh3.googleusercontent.com\/GbAdOJOy074ZiAOwaiX_L3F6g-AOTn9P2cKEJrERWFG-bBzi13XjedBrh6-4WkHbiJ5qf3hXlSPXcprGoyiOBtKi5UXgzzX6F17GSnrLsYc0wmN3wWCmBGo0ygdX6SPdh1Ko5kvhOdkZ98Skziu70zLg2qClkYMM-g0DiOE8yM16T5Fa20NM97ObNIVE\" alt=\"\"\/><\/figure>\n\n\n\n<p>We designed a way to test dbt models (the black box). Our main drivers are:<\/p>\n\n\n\n<ul>\n<li>Introduce a few changes so that new or mature projects can start using it, without breaking existing behavior.<\/li>\n\n\n\n<li>Find a way to define test scenarios and identify which of them failed.<\/li>\n<\/ul>\n\n\n\n<p>We start by introducing the following macros:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1{%- macro ref_t(table_name) -%}\n\n2&nbsp; &nbsp; {%- if var('model_name','') == this.table -%}\n\n3&nbsp; &nbsp; &nbsp; &nbsp; {%- if var('test_mode',false) -%}\n\n4&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- if var('test_id','not_provided') == 'not_provided' -%}\n\n5&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- do exceptions.warn(\"WARNING: test_mode is true but test_id is not provided, rolling back to normal behavior\") -%}\n\n6&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {{ ref(table_name) }}&nbsp;\n\n7&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- else -%}\n\n8&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- do log(\"stab ON, replace table: &#91;\"+table_name+\"] --&gt; &#91;\"+this.table+\"_MOCK_\"+table_name+\"_\"+var('test_id')+\"]\", info=True) -%}\n\n9&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {{ ref(this.table+'_MOCK_'+table_name+'_'+var('test_id')) }}\n\n10&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- endif -%}\n\n11&nbsp; &nbsp; &nbsp; &nbsp; {%- else -%}\n\n12&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {{ ref(table_name) }}&nbsp;\n\n13&nbsp; &nbsp; &nbsp; &nbsp; {%- endif -%}\n\n14&nbsp; &nbsp; {%- else -%}\n\n15&nbsp; &nbsp; &nbsp; &nbsp; {{ ref(table_name) }}&nbsp;\n\n16&nbsp; &nbsp; {%- endif -%}\n\n17&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n\n18{%- endmacro -%}\n\n19\n\n20{%- macro source_t(schema, table_name) -%}\n\n21\n\n22&nbsp; &nbsp; {%- if var('model_name','') == this.table -%}\n\n23&nbsp; &nbsp; &nbsp; &nbsp; {%- if var('test_mode',false) -%}\n\n24&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- if var('test_id','not_provided') == 'not_provided' -%}\n\n25&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- do exceptions.warn(\"WARNING: test_mode is true but test_id is not provided, rolling back to normal behavior\") -%}\n\n26&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {{ builtins.source(schema,table_name) }}\n\n27&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- else -%}\n\n28&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- do log(\"stab ON, replace table: &#91;\"+schema+\".\"+table_name+\"] --&gt; &#91;\"+this.table+\"_MOCK_\"+table_name+\"_\"+var('test_id')+\"]\", info=True) -%}\n\n29&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {{ ref(this.table+'_MOCK_'+table_name+'_'+var('test_id')) }}\n\n30&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {%- endif -%}\n\n31&nbsp; &nbsp; &nbsp; &nbsp; {%- else -%}\n\n32&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {{ builtins.source(schema,table_name) }}\n\n33&nbsp; &nbsp; &nbsp; &nbsp; {%- endif -%}\n\n34&nbsp; &nbsp; {%- else -%}\n\n35&nbsp; &nbsp; &nbsp; &nbsp; {{ builtins.source(schema,table_name) }}\n\n36&nbsp; &nbsp; {%- endif -%}\n\n37&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n\n38{%- endmacro -%}<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<p>The macros are able to <strong>optionally<\/strong> change the behavior of the macros of source and ref.<\/p>\n\n\n\n<ul>\n<li><strong><em>model_name<\/em><\/strong>: refers to the model actually been tested<\/li>\n\n\n\n<li><strong><em>test_mode<\/em><\/strong>: is a flag that helps identifying if the test_mode is enabled<\/li>\n\n\n\n<li><strong><em>test_id<\/em><\/strong>: the test scenario that is going to be mocked<\/li>\n\n\n\n<li><strong><em>table_name(argument)<\/em><\/strong><strong>:<\/strong> is the source table that is either going to be the true source, or we stab it and use one of our own.<\/li>\n<\/ul>\n\n\n\n<p><strong><em>Prefer multiple small test cases over few large test cases<\/em><\/strong><\/p>\n\n\n\n<p>Test cases should <strong>test something specific.<\/strong> Generating Mock data that contain hundreds of records that test multiple business rules should be avoided. Should the test case fail, it should be easy to identify the cause and its impact.<\/p>\n\n\n\n<p>Suppose we would like to create a test_id with the name: MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE for our model VIDEOS_INFO_SUMMARY which uses a source VIDEOS_INFO<\/p>\n\n\n\n<p>We create a new folder under seeds MOCK_VIDEOS_INFO_SUMMARY<\/p>\n\n\n\n<ol>\n<li>We create the input seed seeds\/MOCK_VIDEOS_INFO_SUMMARY\/VIDEOS_INFO_SUMMARY_MOCK_VIDEOS_INFO_MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE.csv which plays the role of input<\/li>\n<\/ol>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1VIDEO_ID,DATE_REV,CATEGORY,REVENUE&nbsp;\n\n2video_a,2022-02-12,advertisement,10\n\n3video_a,2022-02-12,advertisement,0\n\n4video_a,2022-03-12,other,15\n\n5video_a,2022-03-12,advertisement,1<\/code><\/pre>\n\n\n\n<ol start=\"2\">\n<li>We create the output seed seeds\/MOCK_VIDEOS_INFO_SUMMARY\/VIDEOS_INFO_SUMMARY_MOCK_RESULTS_MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE.csv which plays the role of output we would like to have once<\/li>\n<\/ol>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1VIDEO_ID,YEAR,MONTH,REVENUE,COUNTS\n\n2video_a,2022,2,10,1\n\n3video_a,2022,3,1,1<\/code><\/pre>\n\n\n\n<ol start=\"3\">\n<li>We also create a yml seeds\/MOCK_VIDEOS_INFO_SUMMARY\/VIDEOS_INFO_SUMMARY.yml as follows:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>1version: 2\n\n2\n\n3seeds:\n\n4&nbsp; - name: VIDEOS_INFO_SUMMARY_MOCK_RESULTS_MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE\n\n5&nbsp; &nbsp; config:\n\n6&nbsp; &nbsp; &nbsp; enabled: \"{{ var('test_mode', false) }}\"\n\n7\n\n8&nbsp; - name: VIDEOS_INFO_SUMMARY_MOCK_VIDEOS_INFO_MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE\n\n9&nbsp; &nbsp; config:\n\n10&nbsp; &nbsp; &nbsp; enabled: \"{{ var('test_mode', false) }}\"<\/code><\/pre>\n\n\n\n<p>Notice that the seeds are created only on test_mode. This allows us to omit creating those seeds on default behavior.<\/p>\n\n\n\n<ol start=\"4\">\n<li>Now we define the test inside our yml model definition:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>1models:\n\n2&nbsp; - name: VIDEOS_INFO_SUMMARY\n\n3&nbsp; &nbsp; description: \"Summary of VIDEOS_INFO\"\n\n4&nbsp; &nbsp; tests:\n\n5&nbsp; &nbsp; &nbsp; &nbsp; - dbt_utils.equality:\n\n6&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tags: &#91;'test_VIDEOS_INFO_SUMMARY_MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE']\n\n7&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; compare_model: ref('VIDEOS_INFO_SUMMARY_MOCK_RESULTS_MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE')\n\n8&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; compare_columns:\n\n9&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - VIDEO_ID\n\n10&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - YEAR\n\n11&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - MONTH\n\n12&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - REVENUE\n\n13&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - COUNTS\n\n14&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; enabled: \"{{ var('test_mode', false) }}\"<\/code><\/pre>\n\n\n\n<ol start=\"5\">\n<li>Our model:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>1{{\n\n2&nbsp; &nbsp; config\n\n3&nbsp; &nbsp; (\n\n4&nbsp; &nbsp; &nbsp; &nbsp; materialized = 'table'\n\n5&nbsp; &nbsp; )\n\n6}}\n\n7\n\n8SELECT\n\n9 VIDEO_ID,\n\n10 YEAR(DATE_REV) AS YEAR,\n\n11 MONTH(DATE_REV) AS MONTH,\n\n12 SUM(REVENUE) REVENUE,\n\n13 COUNT(*) COUNTS\n\n14FROM\n\n15 {{ source_t('MY_SCHEMA','VIDEOS_INFO') }}\n\n16WHERE\n\n17 CATEGORY = 'advertisement'\n\n18 AND REVENUE &gt; 0\n\n19GROUP BY&nbsp;\n\n20 VIDEO_ID,\n\n21 YEAR(DATE_REV),\n\n22 MONTH(DATE_REV)<\/code><\/pre>\n\n\n\n<p>Notice the source_t usage instead of using the default source macro.<\/p>\n\n\n\n<p>Now in order to follow the test process we have to go through the following process.<\/p>\n\n\n\n<ol>\n<li>Load up our seeds as:<\/li>\n<\/ol>\n\n\n\n<p>1dbt seed &#8211;full-refresh -m MOCK_VIDEOS_INFO_SUMMARY &#8211;vars &#8216;{&#8220;test_mode&#8221;:true}&#8217;<\/p>\n\n\n\n<ol start=\"2\">\n<li>Then execute our model as:<\/li>\n<\/ol>\n\n\n\n<p>1dbt run -m VIDEOS_INFO_SUMMARY &#8211;vars &#8216;{&#8220;test_mode&#8221;:true,&#8221;test_id&#8221;:&#8221;MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE&#8221;,&#8221;model_name&#8221;:&#8221;VIDEOS_INFO_SUMMARY&#8221;}&#8217;<\/p>\n\n\n\n<ol start=\"3\">\n<li>And then execute dbt test to check if our black box behaved as it should:<\/li>\n<\/ol>\n\n\n\n<p>1dbt test &#8211;select tag:test_VIDEOS_INFO_SUMMARY_MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE &#8211;vars &#8216;{&#8220;test_mode&#8221;:true,&#8221;test_id&#8221;:&#8221;MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE&#8221;,&#8221;model_name&#8221;:&#8221;VIDEOS_INFO_SUMMARY&#8221;}&#8217;<\/p>\n\n\n\n<p><strong>Note: Because the whole process is a bit tedious with writing all those big commands, we wrote a bash script which automates all three steps:<\/strong><\/p>\n\n\n\n<p>The requirement is to create a file conf_test\/tests_definitions.csv which has the format:<\/p>\n\n\n\n<p>1# MODEL_NAME,TEST_ID<\/p>\n\n\n\n<p>2VIDEOS_INFO_SUMMARY,MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE<\/p>\n\n\n\n<ol>\n<li>Script reads this file and executes all the tests defined in the file in order<\/li>\n\n\n\n<li>Executing tests of only a specific model is supported by passing -m flag .\/dbt_test.sh -m VIDEOS_INFO_SUMMARY<\/li>\n\n\n\n<li>Executing a specific test case is supported by passing -t flag .\/dbt_test.sh -t MULTIPLE_VIDEOS_HAVE_ZERO_REVENUE<\/li>\n\n\n\n<li>Lines that start with # are skipped<\/li>\n<\/ol>\n\n\n\n<p><strong>In the whole set-up described above there are some conventions that are important to be followed, otherwise the script\/macros might not work<\/strong><\/p>\n\n\n\n<ol>\n<li>The seed folder must be named MOCK_{model_we_test}<\/li>\n\n\n\n<li>The seed which plays the role of input must be named {model_we_test}_MOCK_{model_we_stab}_{test_id}<\/li>\n\n\n\n<li>The result which plays the role of wanted result must be named {model_we_test}_MOCK_RESULTS_{test_id}<\/li>\n<\/ol>\n\n\n\n<p><strong>All the code exists in the following repo:<\/strong><a href=\"https:\/\/github.com\/vasilisgav\/dbt_tdd_example\"> https:\/\/github.com\/vasilisgav\/dbt_tdd_example &#8211; Connect to preview<\/a>&nbsp;&nbsp;<\/p>\n\n\n\n<p>To see it in practice:<\/p>\n\n\n\n<ul>\n<li>set up a tdd_example profile<\/li>\n\n\n\n<li>make sure you run dbt deps to install dbt_utils<\/li>\n\n\n\n<li>make the script executable chmod +x dbt_test.sh<\/li>\n\n\n\n<li>and finally execute the script .\/dbt_test.sh<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">RESULTS:<\/h2>\n\n\n\n<p>What we have found by working with this approach, as it is expected with any TDD approach. The result was a big win into how we release our dbt models<\/p>\n\n\n\n<p>Pros<\/p>\n\n\n\n<ul>\n<li>models have grown to become quite clean with their business clearly depicted<\/li>\n\n\n\n<li>business rules can easily be verified, especially their changes<\/li>\n\n\n\n<li>business voids are identified faster<\/li>\n\n\n\n<li>business requirements are generated in a cleaner, more efficient way<\/li>\n\n\n\n<li>quick development, yes it&#8217;s surprising but we deal with billion of rows, the less runs we are going to perform on the full load of table the quicker the development<\/li>\n\n\n\n<li>regression tests are handled by our github actions ensuring our models behave as expected (multiple puns here \ud83d\ude00 )<\/li>\n\n\n\n<li>QA can happen independently of our dev<\/li>\n\n\n\n<li>Warehouse usage is limited<\/li>\n<\/ul>\n\n\n\n<p>Cons:<\/p>\n\n\n\n<ul>\n<li>Tables sources with multiple columns sometimes are cumbersome to mock, although if columns are not selected then defining them in the mock csv\u2019s is not required<\/li>\n\n\n\n<li>It\u2019s somewhat difficult to start<\/li>\n<\/ul>\n\n\n\n<p>So, what are the key takeaways? That testing is important but good, smart testing can truly free an organization of a lot of daily tedium and allow it, as it has us, to focus more on serving the business efficiently and with the least amount of friction.<\/p>\n\n\n\n<div style=\"height:43px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-layout-1 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full is-resized is-style-rounded\"><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/52.91.248.125\/wp-content\/uploads\/2023\/02\/vasilis_gavriilidis.jpeg\" alt=\"\" class=\"wp-image-1604\" width=\"186\" height=\"186\"\/><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<p><strong><strong>Vasilis Gavriilidis<\/strong><\/strong><\/p>\n\n\n\n<p>Senior Data Engineer @ ORFIUM<\/p>\n\n\n\n<p><a href=\"https:\/\/www.linkedin.com\/in\/vgavriilidis\/\">https:\/\/www.linkedin.com\/in\/vgavriilidis\/<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/vasilisgav\/\">https:\/\/github.com\/vasilisgav\/<\/a><\/p>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Black box testing for non-data engineers with DBT Black box testing is a software testing method in which the functionalities of software applications are tested without having knowledge of internal code structure, implementation details, and internal paths. Let\u2019s borrow that term and use the same analogy to test our black boxes, meaning our dbt models. [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":1988,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_seopress_robots_primary_cat":"none","_seopress_titles_title":"","_seopress_titles_desc":"","_seopress_robots_index":"","content-type":"","footnotes":""},"categories":[19],"tags":[],"acf":[],"_links":{"self":[{"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/posts\/1984"}],"collection":[{"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/comments?post=1984"}],"version-history":[{"count":1,"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/posts\/1984\/revisions"}],"predecessor-version":[{"id":1987,"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/posts\/1984\/revisions\/1987"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/media\/1988"}],"wp:attachment":[{"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/media?parent=1984"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/categories?post=1984"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.orfium.com\/wp-json\/wp\/v2\/tags?post=1984"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}