dbt Testing Interview Questions

Reviewed by Mark Dickie · Last updated

6 questions on this page you can answer and see marked on the spot. Go to the first one

dbt tests are assertions you run against your transformed data to catch broken assumptions before they reach dashboards or downstream consumers. For an interview, you should know the four built-in generic tests (not_null, unique, accepted_values, relationships), how to write a singular test as a SQL query that returns failing rows, and how to define tests in a model's YAML config. You should also understand how dbt test works internally: it compiles each test into a select statement and fails the run if any rows come back. Interviewers often ask about test severity (warn vs error), storing test failures for later review, and when to reach for generic versus singular tests.

What is the difference between generic and singular tests in dbt?

AspectGeneric TestsSingular Tests
DefinitionReusable macros bundled with dbt or custom-writtenA single SQL query in tests/ directory
ConfigurationDefined in YAML under a model or sourceDefined as a standalone .sql file
Return logicMacro builds a query that returns violating rowsThe query itself returns failing rows
ReusabilityApplied to many columns and modelsTied to one specific business rule
Built-in examplesnot_null, unique, accepted_values, relationshipsNone; you write the SQL

How does dbt test compile and execute tests?

  1. dbt parses the project and collects every test defined in YAML configs and in the tests/ directory.
  2. Each generic test is rendered through its macro, producing a select statement that returns violating rows.
  3. Singular tests are already SQL queries, so they pass through with minimal compilation.
  4. dbt runs each compiled query against your warehouse.
  5. If a query returns zero rows, the test passes. If it returns any rows, the test fails (or warns, depending on severity).

When should you use warn vs error severity?

Tests default to error severity, which fails the dbt run immediately and halts downstream model execution. Setting severity: warn in your YAML config lets the test report failures without blocking the pipeline. This is useful for checks on data that matters but is not blocking, for example a not_null test on a column that is usually populated but occasionally has known gaps. You can also configure warn_if to control how many failing rows trigger a warning instead of a hard failure, such as warn_if: ">= 10".

What is the store_failures option and why does it matter?

By default, dbt discards the rows that caused a test to fail after reporting the result. Enabling store_failures: true (globally or per test) writes those failing rows to a table in your warehouse so you can query them later. This is valuable when a test fails in production and you need to inspect the specific bad records to diagnose the root cause. The table is written to a schema you configure, commonly named after the test itself.

Key facts

  • Tarmac has 13 dbt interview questions on this topic, 10 of them on this page, at difficulty 1–5 of 5.
  • Tarmac tracked 428 job postings asking for dbt in September 2026.
  • Roles asking for dbt advertise a median base salary of £80,000, across 257 job postings as of September 2026.
  • Tarmac last reviewed these dbt interview questions on 21 September 2026.

At a glance

Questions10 shown · 13 in the bank
Difficulty1–5 of 5
FormatsMultiple choice, Find the bug, Fill in the blank, Code output, Short answer, Multiple answer, True / false, Flashcard, Ordering

What you'll review

  1. custom generic tests
  2. generic tests
  3. singular tests
  4. dbt build
  5. source freshness

Practice questions

Try one before you open the answer. Pick an option and press Check; it's marked on the spot.

You write a custom generic test in dbt using a {% test %} block and attach it to a column in a schema YAML file. How does dbt decide whether that test passed?#

Options

Show answer

A dbt custom generic test passes when its query returns zero rows. dbt compiles the SQL inside the {% test %} block and runs it against the warehouse. Any rows it returns are counted as failures and reported in the run results.

Why:

dbt compiles a generic test into a SELECT statement and runs it against the warehouse. The test passes when that query returns zero rows, and any returned rows are counted as failures (the failure count is reported in the run results). The other options describe mechanisms dbt does not use for generic tests: dbt does not expect a boolean column, does not execute Python functions for SQL generic tests, and does not compare row counts against values listed in YAML.

This dbt schema.yml is meant to add an accepted_values generic test to the status column of the orders model, allowing only placed, shipped, and returned. One line is wrong and will stop the test from being configured correctly. Select the line number (1-based) of the buggy line.#

version: 2
models:
  - name: orders
    columns:
      - name: status
        tests:
          - accepted_values:
              value: ['placed', 'shipped', 'returned']
Show answer

The bug is on line 8.

Why:

The accepted_values generic test takes its allowed list under the argument name values (plural), not value. On line 8 the key is written as value:, so dbt receives no values argument and reports a missing required argument instead of checking the column. The fix is to rename that key to values:. The other lines (the version key, model name, column name, and the test nesting under tests:) are correctly structured.

In dbt, a singular test is a SQL file in the tests/ directory. When does that test count as failed?#

Options

Show answer

A dbt singular test fails when its SQL query returns one or more rows. Each returned row is treated as a record that violates the assertion. If the query returns zero rows, no violations exist and the test passes.

Why:

dbt runs a singular test's SELECT statement and checks the result. Each row returned is a record that violates the assertion, so if the query returns one or more rows, the test fails. A query that returns zero rows means no violations were found, so the test passes. Singular tests are plain SQL files and do not need a schema.yml entry, and they do not use boolean output to signal failure.

In dbt, two of the four built-in generic tests declared under columns: in a schema.yml file are used as follows. The generic test that fails when a column contains any value outside a fixed list you supply is _____. The generic test that checks every non-null value in a foreign-key column exists in a parent model's column is _____. Give the exact test names as written in YAML.#

Show answer

In dbt, two of the four built-in generic tests declared under columns: in a schema.yml file are used as follows. The generic test that fails when a column contains any value outside a fixed list you supply is **accepted_values**. The generic test that checks every non-null value in a foreign-key column exists in a parent model's column is **relationships**. Give the exact test names as written in YAML.

Why:

dbt ships four generic tests: unique, not_null, accepted_values, and relationships. accepted_values takes a values list and fails any row whose column value is not in that list. relationships takes to: and field: arguments and fails any non-null value that has no matching row in the referenced model's column, which is the referential-integrity check. The other two built-ins, unique and not_null, do not involve a fixed list or a parent model.

Given this schema.yml, the id column of stg_orders contains a NULL and a duplicated value. What is the result of dbt test --select stg_orders?#

models:
  - name: stg_orders
    columns:
      - name: id
        tests:
          - unique
          - not_null

Options

Show answer
Both tests run and both FAIL: the not_null test returns the NULL row and the unique test returns the duplicated value (each failing test reports its offending rows)
Why:

unique and not_null are built-in generic tests. dbt compiles each into a select that returns the failing rows — not_null returns rows where id is null, unique returns values appearing more than once via a group by ... having count(*) > 1. A generic test passes when that query returns zero rows and fails when it returns one or more. With both a NULL and a duplicate present, the two tests run independently and each fails, reporting its own offending rows; dbt does not stop after the first failure — every selected test runs so you see the full picture. The tests check values, not just types (so "both PASS" is wrong), and a column may carry any number of tests (so a compile error is wrong). Generic tests are the cheapest, most common data-quality guardrail in dbt; dbt build would additionally use these failures to skip downstream models.

You have written a custom generic test in dbt as a {% test %} macro, for example {% test not_negative(model, column_name) %} that checks a numeric column for negative values. Explain how dbt decides whether an invocation of this test passes or fails. In your answer, state what the SQL the macro produces must return for a pass by default, what it returns for a failure by default, and how the warn_if and error_if configs can change that behavior.#

Show answer

dbt compiles the generic test macro into a SELECT query and runs it against the warehouse. The query is written to return the rows that violate the rule, for example select * from {{ model }} where {{ column_name }} < 0. By default, if the query returns zero rows the test passes; if it returns one or more rows the test fails (severity permitting) and dbt reports the number of failing rows. The pass condition is not strictly 'zero rows always', however: dbt's warn_if and error_if configs let you set row-count thresholds (e.g. error_if: '>10', warn_if: '>0'). With these, a test returning some rows may still pass if the count stays within the configured threshold, or only warn rather than error.

Why:

dbt treats a generic test as a query that selects violating records. The macro's SQL is run against the model, and by default the test passes when that query returns no rows; any returned row marks a failure and the row count is reported. The warn_if and error_if configs (defaulting to '>0' for error) set thresholds on the returned row count, so a test can still pass or only warn when the count falls within the configured bound rather than always requiring an empty result set. This is why the test is written as a SELECT of violating records, and why the default pass condition corresponds to an empty result set, adjustable via threshold configuration.

Which statements correctly describe how dbt build differs from running dbt run and dbt test separately? Select all that apply.#

Options

Pick every one that applies.

Show answer

dbt build runs seeds, snapshots, models, and tests as one DAG-ordered command, interleaving them node by node: it builds a model, runs that model's tests, and only then moves downstream. A failing test skips the dependent models, so bad data does not propagate — the key advantage over a separate dbt run then dbt test, where everything is built before any test runs. It always respects the DAG and never executes alphabetically or skips tests.

Why:

dbt build is a single DAG-aware command that executes seeds, snapshots, models, and tests together in dependency order, interleaving them node-by-node. Crucially, after building a node it runs the tests attached to that node before proceeding downstream, so a failing test causes dbt to skip the dependent models rather than build them on top of bad data. That early-stop behaviour is the main reason build is preferred in CI/production over a separate dbt run followed by dbt test — in the split workflow, every model is built first, so failing tests are discovered only after bad data has already been written and propagated downstream. The claim that dbt build skips tests entirely is wrong (build very much runs tests), and the claim that it ignores the DAG is wrong (it strictly respects the DAG, never alphabetical order).

dbt source freshness checks how recently raw source tables were loaded by querying a loaded_at_field (or warehouse metadata) and comparing it to warn_after/error_after thresholds — it is about the freshness of external sources, not of dbt's own models.#

Options

Show answer

True. dbt source freshness is about raw external sources, not dbt's own models. It reads each source's loaded_at_field (or warehouse metadata) to find the latest load time and compares the gap to the warn_after/error_after thresholds declared on the source, warning or erroring when ingestion has gone stale. Teams run it before a build to catch a broken upstream load early. It tells you nothing about how current your models are — that depends on your last run.

Why:

True. Source freshness is a property you declare on source definitions in a .yml file, not on models. dbt looks at a loaded_at_field column (or, on supported warehouses, table metadata) to find the most recent load time for each source table, then compares the gap to now against the warn_after and error_after thresholds you configure, emitting a warning or error when raw data has gone stale. This catches a broken or late upstream ingestion (an ETL job that stopped running) before you waste a build transforming stale data — a common pattern is to run dbt source freshness at the start of a pipeline and abort if it errors. It says nothing about whether your dbt models are up to date; that depends on when you last ran them. Because it targets sources, it has no notion of ref() — it is checking the inputs to your project, not its internal nodes.

In dbt, how does a custom generic test (defined with a {% test %} block and applied under a model's tests: or data_tests: key) decide whether it passes or fails? What must its SQL return?#

Show answer

The test's SQL should select the rows that violate the condition. dbt runs it as a query and counts the returned rows: zero rows means the test passes, and one or more rows means it fails (or warns, depending on the configured severity). It is not a boolean or a count the macro returns.

Why:

A dbt generic test is a macro whose body is a SELECT statement. dbt wraps that query and evaluates the result set size, not a returned value. Returning only failing records is the defining convention: an empty result is a pass, and any rows are reported as failures. Severity (error vs warn) then decides whether failures stop the run or just get reported. This is why a custom test that returns a boolean or aggregate scalar would be misread as failing or passing incorrectly.

A custom generic test is a macro named test_<name> that takes model and column_name arguments and is attached to a column in a schema YAML file. When you run dbt test, dbt processes that test through several internal stages. Put these stages in the order dbt performs them, from first to last.#

Put these in order

Show answer

Parse, render, execute, evaluate, report. When dbt test runs a custom generic test, dbt first resolves the test name to its test_<name> macro during parsing. It then renders the macro into a SELECT, runs it against the warehouse, treats any returned rows as failures, and records the outcome in the run results.

Why:

dbt must first resolve the test name from the YAML definition to a macro during parsing, since nothing can be rendered until the macro is known. Next it invokes that macro with the model and column arguments bound to the target relation and column, which yields a SELECT. That query is executed against the warehouse, and the returned row count determines the outcome: zero rows pass, any rows fail. Only after that outcome is known can dbt record the pass or fail status and failure count in the run results. Each adjacent pair is a strict dependency, since each stage consumes the output of the one before it.

Related interview questions

Job market

See dbt salaries and hiring demand from live job postings.

The other 3 questions

This page shows 10 and marks what you pick. That's as far as a page can go. A free account opens the other 3 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.

Start with this topic

Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan

Interview booked? Practise the job ad instead

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.