Testing Coverage¶
If you haven't already create a new branch called dpg/<your_name>/day1. For each exercise, commit your changes with a small explanation (using imperative tense) as a commit message.
eg. "Add relationships tests for touchpoints and contracts FKs"
1. Test coverage¶
- Use dbt Catalog's Recommendations tab, filtered to Testing, to identify the biggest test coverage gaps and get a list of priorities on how to fill those gaps.
- Write the tests yourself to fill those gaps.
- Run dbt build and check those test gaps are closed.
This is normally dbt Wizard's job
In the real world, dbt Wizard can draft this YAML fast once you've identified the gaps. For this training, write it by hand.
2. Test parameters¶
Look at the Customer Relationship Management (CRM) data. There are currently four accepted_values tests. Your CRM systems are very volatile, and you are aware that changes are often seen when the systems gets a new update.
Identify which of these accepted values is most at risk of breaking the full pipeline and make a change that will allow for these kinds of changes. Look in the README.md to learn more about the CRM systems so that you can decide on the correct strategy.
Hint: What kind of changes should I implement?
You will need to decide which tactic you use for each accepted_values test. It will include (but may not be limited to)
- Add more values into the list of values on the test, to account for incoming new values
- Change the SQL code in the staging model to account for inconsistencies
- Add a severity parameter to the test to allow for small changes/updates
3. Adding a test with modelling¶
Write a test to ensure that every member has been charged an applicable amount, according to the fee table below.
- Note that the prices have never changed, so if someone is on the basic plan, the price should be fixed at €599,00
- Note that staff members get a discount of 12.5%
- There is a seed file staff_members.csv which you can use to determine which users are staff members
The normal rates are as follows:
| Plan type | Price |
|-----------|--------------|
| Basic | 599 |
| Premium | 1499 |
| Standard | 999 |
Steps:
dim_subscriptionsinmediapulse_basealready hasplan_typeandmonthly_fee_dollarscalculated for you - but itsmartsareaccess: protected, somediapulse_analyticscan'tref()it directly. Thestaginglayer isaccess: public, so build offstg_streaming__subscriptions_lifecycle_recinstead, using a cross-projectref().- Bring in the
staff_membersseed to work out whichuser_ids should get the staff discount. - The fee table above never changes, so hardcode it (a
casestatement or a small CTE) rather than adding a seed for three static prices. - Write a singular test that computes the fee you'd expect each member to be charged - full price for everyone else, discounted for staff - and returns any row where the fee actually charged doesn't match. Run it and see what it finds.
Hint: rounding
"12.5% off" is ambiguous once you're working in whole cents/euros. Decide deliberately whether you round, floor, or ceiling the discount, and apply it consistently - otherwise you'll get false positives on rows that are actually correct to the cent.
4. Update the documentation¶
Considering what you saw in the README.md in part 2, update the documentation in the _crm__sources.yml file to carry this documentation through into dbt.
Steps:
- Re-read the "Known Caveats and upcoming changes in source" note in
mediapulse_analytics/README.md, and match each caveat to the specific source table and column it affects. - For each affected column in
_crm__sources.yml, extend thedescription:so it captures what a future reader can't tell from the column name alone: the unitcontract_value_centsis actually stored in, the fact that all the timestamp fields are in local server time rather than UTC, and the upcoming value/casing changes onaccount_status,renewal_status,contract_tier, andtouchpoint_type. - Write these updates yourself, then do your own line-by-line pass against the README afterward - this is exactly the kind of drafting dbt Wizard would normally do for you, but for this training you're writing it by hand and checking it yourself, to make sure you haven't missed (or invented) a caveat.
- Decide whether any of this documentation also belongs on the
stg_crm__*models themselves (in_crm__models.yml), not just on the source - that's what most of your teammates will actually read day-to-day.
Hint: documentation lives with the reader, not just the source
A source's description isn't automatically inherited by the staging models built on top of it. If a caveat matters to someone querying stg_crm__advertiser_accounts directly, consider documenting it there too, not only on the raw source table.
5. Overwrite a built-in test¶
In some bits of the data, "N/A" shows up where it really means NULL - but it's stored as the literal string, not an actual null value. Right now, running not_null against a column like this passes even though the data is functionally missing, because "N/A" is a non-null string as far as SQL is concerned.
Goal: override dbt's built-in not_null test so it can optionally treat specific string values as if they were null, without losing any of its normal behavior.
The overridden test should:
- Behave exactly like the standard
not_nulltest when called with no extra arguments - Accept an optional
null_valuesparameter - a list of strings (e.g.["N/A", "n/a", "-"]) - that should be treated as null for that specific test configuration - Only apply this extra leniency where
null_valuesis explicitly passed, so every othernot_nulltest in the project keeps working exactly as it does today
Hint: Find out how dbt resolves not_null today
Before you can override a built-in test, you need to know where it's actually defined and how dbt decides which version of a test to run when more than one definition with the same name exists in scope.
dbt's built-in tests aren't special-cased - they're macros named test_<test_name>,
resolved through the same dispatch mechanism as any other macro.
See Writing custom generic tests for how a project-level macro with the same name takes precedence over the global one.
Hint: Write the overriding macro
Create a macro in your project that overrides not_null. It needs to accept the same arguments
the built-in version does, plus your new null_values argument, and give null_values a sensible
default so existing calls to not_null elsewhere in the project don't break.
See Jinja and macros for how macro arguments
and default values are declared - this is what lets null_values be optional.
Hint: what the original test actually checks
Before you can extend the logic, you need to know exactly what condition the built-in not_null
test asserts failure on. The Add tests to your DAG
page explains how generic tests are structured as a select that returns failing rows -
your override needs to return failing rows using the same shape, just with an extended definition of "null."
Hint: Apply it with null_values
Pick a column in the project that has this "N/A"-instead-of-null problem, and configure a not_null
test on it that passes in your new null_values argument.
Generic test arguments are set under a column's data_tests: key in the model's YAML.
See Add tests to your DAG for the syntax for passing extra configuration into a test.
Hint: Confirm both behaviors
Run the test you just configured and confirm it correctly treats "N/A" as missing.
Then find (or temporarily add) a second not_null test elsewhere in the project that does not pass null_values,
and confirm it still behaves exactly as before - i.e. a genuine empty string or other non-null junk value still passes,
since only actual NULLs and your specified null_values should fail the test.
You don't need to run the whole project to check this. dbt test accepts a --select flag targeting a specific
model or test, so you can check both cases quickly.
See Node selection syntax.
6. Tag your tests, and store what breaks¶
Not every test matters equally, and a bare pass/fail count doesn't always tell you enough to debug a failure.
Steps:
- Add a
tagsconfig (e.g.important) to a handful of your most critical tests - the ones guarding a primary key, or that should never be allowed to fail quietly - without tagging everything. - Run
dbt build --select tag:important(ordbt test --select tag:important) and confirm only those tagged tests run. - Look at
fct_streaming_eventsand pick one of the enriched fields that comes in through the join to the content catalog -ctnt_type,release_date, orruntime_minutes- that can come back null when the join doesn't find a match. Add anot_nulltest on it, and configurestore_failures: trueon that test. - Run it, then query the failures table dbt just created. What do the failing rows have in common?
Hint: tags aren't just labels
Tags slice a test run the same way --select does for models: dbt build --select tag:important only runs resources tagged important, wherever they live in the project. You can set a tags config at the column-test level, the model level, or in dbt_project.yml for a wide sweep - keep the set small enough that "important" still means something.
Hint: where the failures actually land
store_failures: true writes every failing row to a real table in your target schema instead of discarding them once the test finishes. That's the difference between "12 rows failed" and being able to see whether those 12 rows share a content_id, a date range, or something else worth chasing down.
Done?
You've closed real coverage gaps at scale, by:
- matching the right tactic to each
accepted_valuestest based on how the CRM source is actually changing - writing a test that checks real business logic rather than a single column in isolation
- carrying a vendor's caveats through into the project's own documentation
- overriding a built-in generic test without breaking its existing callers
- setting up tagging and
store_failuresso the team can run what matters most and actually see what a failure looked like