Testing guide#
How this package is tested, how to run the tests, and what the results should look like.
Scope today: Snowflake, on dbt v2 (the dbt Fusion engine) and dbt v1 (dbt-core 1.10 and later). The same commands and expected results apply to both engines. The Databricks, BigQuery, and Redshift models are in beta and have their own data tests. Their unit tests are planned.
1. What each kind of test proves#
| Test type | Count | What it proves | Needs real account data? | Where it lives |
|---|---|---|---|---|
| Data tests | 56 on Snowflake marts, 2 on int_snowflake__clustering_key_summary, plus 3 on shared models and 1 savings check on the recommendation backlog (warns) |
The real output has no null keys, and category columns only contain expected values | Yes. Run after building the models in a real project. | data_tests: blocks in models/snowflake/marts/**/_*.yml, models/snowflake/intermediate/_snowflake_intermediate.yml and models/shared/_shared.yml |
| Unit tests | 22 | The transformation logic is correct: for given input rows, a model returns exactly the expected output rows | No. Inputs are supplied in the test. A warehouse connection is still required. Off by default; see §3. | models/snowflake/marts/gold/_gold__unit_tests.yml, models/snowflake/marts/warehouse/_warehouse__unit_tests.yml, models/snowflake/intermediate/_snowflake_intermediate__unit_tests.yml |
| Macro tests | 5 | Macros that return values (scores, arrays, generated config text, the scaling-efficiency curve) return exactly what's expected | No. A warehouse connection is still required. Off by default; see §3. | tests/snowflake/macros/ |
| Integration tests (Snowflake) | 19 assertions, on Standard and Enterprise edition | The whole pipeline, hooks included, turns known account activity into exactly the expected recommendations, savings, and gold-view rows | No. Fixture tables stand in for ACCOUNT_USAGE. Some fixtures run real queries in your account (see §3). |
integration_tests/snowflake/ |
| Smoke test (Snowflake) | every package model and data test, both editions | Every model compiles and runs against the real ACCOUNT_USAGE views' columns and types, on a full refresh and an incremental run |
Reads the real views' structure only (--empty) |
Commands in §3 |
What the unit tests cover:
- All 11 Snowflake gold views: filtering, deduplication, ranking, domain and action mapping, cost aggregation, and credit attribution.
scope_filter, the project-scoping logic behind every gold view. There's one test for each combination ofdbt_monitored_projects([], a list,['*']) andinclude_full_platform_insights.- Regression tests for bugs found and fixed while writing the tests. Each one fails against the old SQL:
vw_snowflake__warehouse_optimizationslabeled mixed actionable/monitor groupsmonitor.vw_snowflake__top_queried_modelsreturned no rows withdbt_monitored_projects: ['*'].vw_snowflake__top_spillage_modelsduplicated a model that had several spillage signals.vw_snowflake__user_level_cost_attributiondouble-counted queries that read several models, and applied one warehouse rate to all of a user's time.
Every unit test and macro test has been checked to fail when the logic it covers is broken, not just to pass. So has every integration assertion: each fix it covers was reverted (a "mutation check") and the assertion failed.
2. Before you start#
- dbt v2 (the dbt Fusion engine), or dbt v1: dbt-core 1.10 or later with dbt-snowflake. The suites below run on both. On dbt v1 they were run on dbt-core 1.11.7 and 1.10.23. To run dbt v1 without changing your installed dbt, use
uvx, for exampleuvx --from 'dbt-core~=1.11.0' --with 'dbt-snowflake~=1.11.0' dbt <command>. - A Snowflake role that can read
SNOWFLAKE.ACCOUNT_USAGE(IMPORTED PRIVILEGESon theSNOWFLAKEdatabase), plus a warehouse. - A dedicated test schema. Setting up unit tests builds tables (mostly empty) in it. Because the package sets
+schema: dbt_cost_optimization, models land in<your target schema>_dbt_cost_optimization. - A dbt profile target for this repo's profile,
dbt_cost_optimization, in~/.dbt/profiles.yml. Key-pair authentication works well for repeated test runs:
# ~/.dbt/profiles.yml (outside the repo). Placeholder values only.
dbt_cost_optimization:
target: test
outputs:
test:
type: snowflake
account: <org>-<account>
user: <username>
role: <role with ACCOUNT_USAGE access>
warehouse: <warehouse>
database: <database>
schema: <test schema>
private_key_path: <path to your .p8 key>
threads: 8
Never commit credentials. Keep profiles and keys outside the repo.
target/andlogs/are gitignored because compiled SQL and logs contain database and schema names.
The package's models are disabled by default, so every command below turns them on:
- From this repo (unit and macro tests), the package is the root project, so
--vars '{dbt_cost_optimization_enabled: true}'turns it on. - Where the package is installed (the data tests, the integration tests and the smoke test), it's turned on with the environment variable
DBT_COST_OPTIMIZATION_ENABLED=true, which works on both engines (on dbt v1, the package'sdbt_project.ymlcan't read vars). Standard edition also needsDBT_COST_OPTIMIZATION_SNOWFLAKE_ENTERPRISE_EDITION=false.
Add --target <your target> if your test target isn't the profile's default.
3. Running the tests#
Data tests: in a project that installs the package#
Data tests check real output, so run them where the package analyzes real activity: in a dbt project that installs this package and has models with query history.
To run only the tests against models you've already built:
Unit and macro tests: from this repo#
Unit tests read the column types of each tested model's parents from the warehouse, so the parents have to exist first. They can be empty. This is a one-time setup per test schema, and you only need to repeat it when a parent model's columns change.
# 1. Build everything the gold views depend on as empty tables, except the two models
# below and anything downstream of them.
dbt run --select +tag:gold \
--exclude int_snowflake__warehouse_config+ fct_snowflake__table_clustering_candidates+ \
--empty --vars '{dbt_cost_optimization_enabled: true}'
# 2. Build those two models normally. Their inputs are empty, so this is quick.
dbt run --select int_snowflake__warehouse_config fct_snowflake__table_clustering_candidates \
--vars '{dbt_cost_optimization_enabled: true}'
# 3. Build everything else as empty tables.
dbt run --select +tag:gold \
--exclude int_snowflake__warehouse_config fct_snowflake__table_clustering_candidates \
--empty --vars '{dbt_cost_optimization_enabled: true}'
Why two models are built normally:
--emptyrewrites everyref()as(select * from … where false limit 0). That works in a model'sselect, but not in post-hooks that write to a table, which these two models have (refresh_warehouse_config,extract_operator_evidence,refresh_column_cardinality). Building them normally on top of empty inputs avoids the problem.
Then run the tests:
dbt test --select test_type:unit \
--vars '{dbt_cost_optimization_enabled: true, dbt_cost_optimization_run_package_tests: true}'
dbt test --select tag:macro_tests \
--vars '{dbt_cost_optimization_enabled: true, dbt_cost_optimization_run_package_tests: true}'
Macro tests don't read any tables, so they don't need the setup step.
Integration tests (Snowflake): from this repo#
integration_tests/snowflake/ is a dbt project that installs this package from the repo and points its snowflake_usage source at fixture tables instead of SNOWFLAKE.ACCOUNT_USAGE. Its vars are in its dbt_project.yml (dbt v1 doesn't read vars.yml). The source location is passed with --vars on each command, as literal values: it depends on your target, and dbt v1 doesn't render Jinja in var values. The fixtures describe known activity: demo models with query history, builds, warehouse metering and events, spillage, dbt platform jobs, and access history. The assert_* tests in tests/ check what the package recommends from it.
Run it from integration_tests/snowflake/, with the same profile target as the unit tests. Each edition builds into its own schema, <target schema>_integration (Standard) or <target schema>_integration_enterprise:
cd integration_tests/snowflake
dbt deps
export DBT_COST_OPTIMIZATION_ENABLED=true
# Your target's database, and its schema with _fixtures appended (literal values)
SRC='snowflake_usage_database: <target database>, snowflake_usage_schema: <target schema>_fixtures'
# 1. Fixtures and demo models (into <target schema>_fixtures and <target schema>)
export DBT_COST_OPTIMIZATION_SNOWFLAKE_ENTERPRISE_EDITION=false
dbt build --select "+tag:fixtures models/demo" --vars "{$SRC}"
# 2. The package and the assertions, Standard edition: full refresh, then incremental
SEL="+tag:gold +fct_snowflake__clustering_key_candidates +fct_snowflake__warehouse_performance_recommendations"
dbt build --select $SEL --exclude tag:fixtures --full-refresh --vars "{$SRC}"
dbt build --select $SEL --exclude tag:fixtures --vars "{$SRC}"
# 3. The same on Enterprise edition
export DBT_COST_OPTIMIZATION_SNOWFLAKE_ENTERPRISE_EDITION=true
EV="{$SRC, snowflake_enterprise_edition: true, package_test_schema: integration_enterprise}"
dbt build --select $SEL --exclude tag:fixtures --full-refresh --vars "$EV"
dbt build --select $SEL --exclude tag:fixtures --vars "$EV"
The edition is set in both places on purpose: the environment variable switches the Enterprise-only models on, and the var is what the integration project's own assertions read.
What it needs from your account:
- An X-Small warehouse as the target's warehouse. One fixture runs a query sized to spill on X-Small, for the spillage evidence test.
SNOWFLAKE_SAMPLE_DATA, which Snowflake accounts have by default. Another fixture scans it as a query that does disk I/O but doesn't spill.- A few cents of credits per run. The fixtures run real queries: clustering reads, the spill and control queries, and the view-chain demo views the probe measures.
After rebuilding the fixtures, run with
--full-refreshfirst. Fixture timestamps are relative to now, and the package's incremental models would otherwise keep rows from the previous fixtures.
Smoke test (Snowflake): against the real ACCOUNT_USAGE views#
The integration fixtures have only the columns the package reads. The smoke test builds every package model against the real SNOWFLAKE.ACCOUNT_USAGE views with --empty, which catches a renamed column or a changed type. It uses the same three-step order as the unit test setup, and leaves out the integration project's own assertions, which need fixture data:
cd integration_tests/snowflake
export DBT_COST_OPTIMIZATION_ENABLED=true DBT_COST_OPTIMIZATION_SNOWFLAKE_ENTERPRISE_EDITION=false
V='{snowflake_usage_database: SNOWFLAKE, snowflake_usage_schema: ACCOUNT_USAGE, package_test_schema: smoke_standard, snowflake_enterprise_edition: false}'
dbt build --select package:dbt_cost_optimization \
--exclude int_snowflake__warehouse_config+ fct_snowflake__table_clustering_candidates+ test_type:singular \
--empty --full-refresh --vars "$V"
dbt build --select int_snowflake__warehouse_config fct_snowflake__table_clustering_candidates \
--exclude test_type:singular --full-refresh --vars "$V"
dbt build --select package:dbt_cost_optimization \
--exclude int_snowflake__warehouse_config fct_snowflake__table_clustering_candidates test_type:singular \
--empty --full-refresh --vars "$V"
Run it again without --full-refresh for the incremental path, then repeat both with DBT_COST_OPTIMIZATION_SNOWFLAKE_ENTERPRISE_EDITION=true and package_test_schema: smoke_enterprise, snowflake_enterprise_edition: true.
Why the extra var: unit and macro tests check the package itself, so they're off by default. Otherwise they'd run in every
dbt buildof a project that installs the package.dbt_cost_optimization_run_package_tests: trueturns them on.
4. Expected results#
| Command | Expected summary |
|---|---|
| Unit tests | 22 total \| 22 success |
| Macro tests | 5 total \| 5 success |
| Integration tests, Standard edition (each run) | 143 total \| 143 success (models, data tests and assertions) |
| Integration tests, Enterprise edition (each run) | 149 total \| 149 success |
| Smoke test (each step) | all success, no errors |
| Data tests, in an installing project on Snowflake | all pass: 56 Snowflake mart tests, 2 on an intermediate model, plus 3 on shared models. The savings check (assert_snowflake__savings_do_not_exceed_cost) passes, or warns if a cost formula is off |
The counts grow as tests are added, so treat the run summary as the source of truth.
A failing unit test prints a row-by-row diff, with expected -> actual for each value that differs, and ∅ for a missing or extra row. For example:
| PRIORITY_RANK | SIGNAL_ID | RELATED_SIGNALS_COUNT |
| 2.0 | add_clustering_key_strong | 1.0 -> 2.0 |
5. Writing new tests#
- Put unit tests next to the models they test, in a
_<folder>__unit_tests.ymlfile. Give each one adescription:that lists its cases, so reviewers can see the coverage without reading the SQL. - Give every row the same columns in
expect:rows. A column left out of some rows is compared as null in those rows. - Give every parent an input. Use
rows: []for parents a case doesn't need. - Keep time windows stable. For rows inside a "last N days" window, use dates far in the future (
2099-01-01). For rows that should fall outside it, use dates far in the past (2000-01-01). The tests then don't go stale. - Use
overrides: vars:to test var-driven behavior, such as thescope_filtersettings. - Prove the test can fail. Change one expected value, or break the logic the test covers, and confirm it fails with a clear diff. Then restore it.
- Integration fixtures:
- Type each fixture from the real view. Start it with
select <columns> from snowflake.account_usage.<view> where false union all ..., so its columns take the real types and the build fails fast if Snowflake renames one. - Use relative timestamps (
dateadd(..., current_timestamp())), so rows always fall inside the package's lookback windows. - Use real query IDs where a hook reads them.
GET_QUERY_OPERATOR_STATSrejects IDs that don't exist, so the fixture runs the query withrun_queryand recordslast_query_id(). Add a-- depends_on: {{ ref(...) }}hint for anyref()used only inside{% if execute %}. - Keep slices from changing each other. Builds that one slice needs, but that shouldn't count as dbt sessions, run from session 1 (no dbt session), so they don't change the user attribution or expensive-query results.
- Make assertions edition-aware with
{% if var('snowflake_enterprise_edition', true) %}wherever Enterprise should differ. Everywhere else, both editions must give the same result. - Where a value comes from a real measurement (the view probe, operator stats), assert it's consistent with the measurement rather than a fixed number.
- Put macro tests in
tests/snowflake/macros/. They're enabled and tagged by thedata_tests: dbt_cost_optimization: snowflake: macros:block indbt_project.yml. Keep that config scoped tomacros:: config underdata_tests:also applies to generic tests defined in the models' YAML when the paths overlap.
6. Not covered by automated tests yet#
| Area | How it's checked today |
|---|---|
Post-hooks against real SHOW WAREHOUSES settings |
The integration tests run every hook, but fixture warehouses don't exist in the account, so the multi-cluster settings come from a simulated SHOW WAREHOUSES hook (integration_tests/snowflake/macros/simulate_show_warehouses.sql). Check the real path manually (see the checklist below). |
The 7 dbt run-operation commands |
Manual: they query SNOWFLAKE.ACCOUNT_USAGE directly, so test inputs can't reach them (see the checklist below). |
| Databricks, BigQuery, Redshift | Data tests only, for now |
Planned: CI that runs dbt parse on every platform.
Manual checklist: hooks#
Build the package in a real project, then check:
- [ ]
fct_snowflake__incremental_config_recommendations: the log showsprobe_unique_key_candidates: starting exact uniqueness probe. Where there are merge candidates,likely_unique_keyis filled in. - [ ]
fct_snowflake__table_clustering_candidates: the log showsextract_operator_evidence:andrefresh_column_cardinality:messages.int_snowflake__query_operator_evidenceandint_snowflake__column_cardinalityhave fresh rows. - [ ]
int_snowflake__warehouse_config: the log showsrefresh_warehouse_config: merged config for all warehouses, andauto_suspend,scaling_policy, and the cluster counts are filled in. A warehouse withmax_cluster_count > 1hasis_multiclustertrue. - [ ]
int_snowflake__view_probe: the log showsprobe_view_recompute:messages, and views in view chains have rows withprobe_status = 'ok'. - [ ]
fct_snowflake__warehouse_performance_recommendations(Enterprise): the log shows a per-tableextract_spill_evidence: ... analyzed N of M sampled spilling queriesline, andint_snowflake__query_spill_evidencehas rows.
Manual checklist: run-operation commands#
Run each command with its defaults, then with at least one argument override. Confirm that the Criteria: line in the log reflects the override. Do this on both a Snowflake Enterprise and a Standard edition account.
dbt run-operation find_table_clustering_candidates --args '{lookback_days: 14}'
dbt run-operation suggest_clustering_keys --args '{model_name: <a model>, include_boolean_cols: true}'
dbt run-operation find_table_materialization_candidates --args '{lookback_days: 7, min_query_count: 30}'
dbt run-operation find_incremental_materialization_candidates --args '{min_table_size_gb: 50}'
dbt run-operation find_warehouse_sizing_recommendations --args '{lookback_days: 14}'
dbt run-operation find_spillage_candidates --args '{lookback_days: 14}'
dbt run-operation find_expensive_dbt_queries --args '{top_n: 10}'
On a platform without a Snowflake implementation, each command should fail with … is not yet implemented for '<platform>'.