DVT TEST
dvt test works exactly as you know it from dbt — same flags, same selection syntax, same pass/fail semantics, handed to the engine unchanged. What DVT adds is awareness of where your data lives: federated models are tested where they landed, and a test the engine genuinely cannot reach is skipped loudly by name instead of dying with a riddle.
FIRST RUN
Run it after dvt run, once there's something built to assert about:
$ dvt test Found 3 models, 5 data tests, 1 seed, 2 sources Concurrency: 4 threads (target='pg_dev') 1 of 5 START test accepted_values_stg_orders_status ...... [RUN] 1 of 5 PASS accepted_values_stg_orders_status ............ [PASS in 0.09s] 2 of 5 START test assert_no_negative_amounts ............. [RUN] 2 of 5 PASS assert_no_negative_amounts ................... [PASS in 0.06s] 3 of 5 START test not_null_stg_orders_order_id ........... [RUN] 3 of 5 PASS not_null_stg_orders_order_id ................. [PASS in 0.05s] 4 of 5 START test relationships_stg_orders_customer_id ... [RUN] 4 of 5 PASS relationships_stg_orders_customer_id ......... [PASS in 0.08s] 5 of 5 START test unique_stg_orders_order_id ............. [RUN] 5 of 5 PASS unique_stg_orders_order_id ................... [PASS in 0.07s] Done. PASS=5 WARN=0 ERROR=0 SKIP=0 TOTAL=5
Every test is a query that selects violating rows — zero rows back means the assertion holds. Writing them (generic yml tests, singular SQL tests, custom generics) is covered on the Tests page; this page is about running them.
Like every graph command, dvt test begins from a passing dvt parse — a project with a parse error is refused with that error itself, not a pile of confusing downstream failures.
WHEN A TEST FAILS
A failure tells you three things: how many rows violated the assertion, which file declared it, and where the compiled SQL landed — so you can run the failing query yourself:
$ dvt test --select stg_orders 1 of 3 START test accepted_values_stg_orders_status ...... [RUN] 1 of 3 FAIL 12 accepted_values_stg_orders_status ......... [FAIL 12 in 0.11s] 2 of 3 START test not_null_stg_orders_order_id ........... [RUN] 2 of 3 PASS not_null_stg_orders_order_id ................. [PASS in 0.05s] 3 of 3 START test unique_stg_orders_order_id ............. [RUN] 3 of 3 PASS unique_stg_orders_order_id ................... [PASS in 0.07s] Completed with 1 error: Failure in test accepted_values_stg_orders_status (models/staging/schema.yml) Got 12 results, configured to fail if != 0 compiled code at target/compiled/analytics/models/staging/schema.yml/accepted_values_stg_orders_status.sql Done. PASS=2 WARN=0 ERROR=1 SKIP=0 TOTAL=3
The exit code is nonzero on any error-severity failure, so CI catches it without parsing output. Whether a failure is an error at all is the test's own decision — severity: warn, warn_if and error_if are yml configs on the test, not flags on this command. A warn-severity failure lands in the WARN column of the tally and never blocks the run. The knobs are on the Tests page.
SELECTING WHICH TESTS RUN
The selection syntax is the one every DVT command shares — and selecting a model selects the tests attached to it:
dvt test # every test in the project dvt test --select stg_orders # tests attached to one model dvt test --select source:crm # tests attached to one source dvt test --select test_type:singular # only singular tests dvt test --exclude tag:slow # everything except the slow ones
--indirect-selection tunes how tests adjacent to your selection are picked up: eager (the default) is most inclusive, cautious most exclusive, buildable sits in between, and empty selects no tests at all. When a test spans two models — a relationships test, say — eager runs it if either side is selected, cautious only if both are.
WHERE TESTS RUN: THE DEFAULT TARGET
Tests compile and execute against the default target— the engine your profile points at. For models that's exactly right, even in a federated project: whether a table was built natively or landed by an f_table pulling from three other engines, the relation lives on the default target, and its tests run there — federated models are tested where they landed.
Source tests are the honest exception. A test on a source declared on another connection — meta.connection: oracle_ops while your default target is Postgres — asserts against a relation the default engine cannot reach. When tests run inside dvt build, DVT refuses to let that fail an honest build: it skips exactly those tests, loudly, by name, with the reason:
WARNING: Skipping 2 source test(s) on non-default connections (the engine cannot reach them from 'pg_dev'): not_null_crm_customers_customer_id, unique_crm_customers_customer_id
The warning is counted — it shows up in the run's WARN tally, never silently swallowed. A bare dvt testpasses your selection straight through to the engine, so aiming it directly at a foreign source surfaces the engine's own error instead — keep those out of the selection with --exclude source:crm, or better: test that data where it lands, on a bronze model over the source. The Tests page shows the pattern.
IN PLAIN DBT
This situation can't even be expressed — one project speaks to one engine, full stop. Bolt a second engine on sideways and a test against it dies with a raw relation does not exist that never explains itself. DVT knows which connection every source lives on, so it can tell you which tests it skipped and why— by name, with the connection that can't be reached.
KEEPING THE FAILING ROWS: --STORE-FAILURES
A failing count tells you something is wrong; the rows tell you what. --store-failureswrites each test's violating rows to an audit table in your warehouse — one table per test, named after the test, in an audit schema beside your target schema:
$ dvt test --select stg_orders --store-failures 1 of 3 FAIL 12 accepted_values_stg_orders_status ......... [FAIL 12 in 0.14s] ... $ dvt exec -t pg_dev "select * from analytics_dbt_test__audit.accepted_values_stg_orders_status limit 3" value_field | n_records ------------+---------- refnded | 7 SHIPPED | 5 2 row(s) from pg_dev
A typo and a casing bug — found with one query, no re-running the test by hand. dvt exec is the natural way in. DVT records these audit tables in its materialization ledger on pass and fail alike — an empty audit table is still a table — so dvt generate-sources never mistakes one for raw source data. To store failures for one test always, not per-invocation, set store_failures: truein that test's config instead.
REFERENCE — THE FLAGS
--store-failures is the flag that belongs to test alone; the rest are the engine-wide options every graph command shares, with their exact spellings and every alias intact:
| FLAG | TYPE | DEFAULT | WHAT IT DOES |
|---|---|---|---|
| --store-failures | flag | off | Store test results — the failing rows — in the database, so you can query what went wrong. One audit table per test, named after the test. |
| -s, -m, --select, --models, --model | tuple | — | Specify the nodes to include. Selecting a model selects its tests with it. |
| --exclude | tuple | — | Specify the nodes to exclude. |
| --selector | text | — | The selector name to use, as defined in selectors.yml. |
| --indirect-selection | eager | cautious | buildable | empty | eager | Choose which tests to select that are adjacent to selected resources. Eager is most inclusive, cautious is most exclusive, buildable is in between, and empty includes no tests at all. |
| -t, --target | text | (profile default) | Which target to load for the given profile — the engine tests compile and run against. |
| --profile | text | (from dbt_project.yml) | Which existing profile to load. Overrides the setting in dbt_project.yml. |
| --project-dir | path | . | Which directory to look in for dbt_project.yml. Default is the current working directory and its parents. |
| --profiles-dir | path | (resolved) | Where to find profiles.yml. If not set, the project directory is tried first, then ~/.dbt/. |
| --vars | YAML | — | Supply variables to the project, overriding variables defined in dbt_project.yml. A YAML string, e.g. '{my_variable: my_value}'. |
| --threads | integer | (from profiles.yml) | Number of threads to use while executing tests. Overrides the setting in profiles.yml. |
| -x, --fail-fast / --no-fail-fast | flag | off | Stop execution on the first failure. |
| --state | directory | — | Use this state directory for both state comparison and deferral — e.g. dvt test --select state:modified+ --state prod-artifacts/. |
| --defer / --no-defer | flag | off | Resolve unselected nodes by deferring to the manifest within the --state directory. |
| -q, --quiet / --no-quiet | flag | off | Suppress all non-error logging to stdout. |
| --warn-error | flag | off | Where a warning would normally be emitted, raise an exception instead — e.g. a --select that selects nothing, or invalid test configs. |
Those are the ones a test run actually reaches for. The full engine-wide set — logging formats and levels, parsing modes, artifact writing, cache behavior — applies to dvt test exactly as to every other graph command, and dvt test --helpprints all of them, grouped by what you're trying to do.