Skip to main content
SQLBuild supports SQL-native unit tests that validate model logic by comparing actual query results against expected values. Tests can chain across multiple models (multi-model tests), use macros for reusable mock data, and include zero-row assertions. For end-to-end testing across many models with physical warehouse relations, see Scenarios.

How tests work

A test file defines mock inputs and expected outputs using CTEs. SQLBuild substitutes the mock data into the real model SQL, executes it, and compares the result against the expected CTE using EXCEPT queries. Zero mismatched rows means the test passes.
The test:
  1. Mocks the raw__orders source with the __source__raw__orders CTE
  2. Runs the real stg_orders model SQL with the mock substituted in
  3. Compares the output against __expected__stg_orders
  4. Passes if row counts match and there are zero mismatched rows
When an expected-output comparison fails, SQLBuild reports the unexpected and missing row counts and best-effort samples from each direction. Samples are deliberately bounded to three rows, 12 columns, and 120 characters per value, and pass through diagnostic redaction. They are also present as structured unexpected_samples and missing_samples in JSON output. Sampling failure never replaces the known test failure. When exactly one row exists in each difference direction and the bounded samples have the same columns, SQLBuild also aligns the rows and reports only changed columns with their redacted actual and expected values. It does not guess an alignment when several rows differ or sampling is incomplete. Before opening a warehouse connection, SQLBuild statically checks fixture shapes when SQL analysis is enabled. Missing columns are reported together with the test path, fixture resource, and models that read them. Statically provable collection-versus-scalar type conflicts identify the affected column and suggest explicit CAST, ARRAY_CONSTRUCT, or PARSE_JSON expressions. SQLBuild never invents missing values. The trailing SELECT 1 is required as a ceremonial closing statement.

CTE conventions

Any CTE without one of these prefixes is treated as a helper CTE, available to all mock and model SQL in the test.

Multi-model tests

Tests can span multiple models in a single file. Mock your sources, define an expected output for the model you care about, and SQLBuild automatically resolves every intermediate model using its real SQL.
SQLBuild topologically sorts the expected models, resolves each intermediate model’s real SQL with mocks substituted, and chains the outputs forward. Every model between the mocked sources and the expected model is computed automatically. Inspect that boundary without opening a warehouse connection:
The resolved plan lists mocked sources, refs, seeds, real models in execution order, expected models, and any unsatisfied leaf dependencies. A mocked ref is called out explicitly because its real model SQL will not execute. Inspection exits non-zero when the plan contains unresolved dependency errors.

Mocking refs and seeds

You can mock models directly with __ref__<name> and seeds with __seed__<name>, not just sources. This skips the model’s real SQL (or the seed’s real CSV data) and provides controlled data instead:
You can mix __source__, __ref__, and __seed__ mocks in the same test. As long as every leaf dependency is satisfied (either by a source mock, a ref mock, a seed mock, or by being in the expected chain), the test resolves.

Multiple expected models

A single test can assert on multiple models. SQLBuild resolves and compares each one independently:
If the expected models form a chain (e.g. stg_orders feeds into fact_orders which feeds into dim_customers), SQLBuild resolves them in dependency order, using the output of earlier steps as input to later ones. When a test defines __expected__model_name output, it may also use the public enums and constants available to that model. A test that checks several models may use the public enums and constants available to each of them. Public names are unique, so those values cannot conflict. Only explicit expected CTEs create grants. A matching test filename, __ref__ mock, or nearby model path does not. Model-private declarations and macros are never granted through expected models.

Macro-powered mocks

Because unit tests are written in SQL, they support macro calls. This lets you write reusable mock generators instead of copy-pasting mock data across test files:
The @mock_orders() call expands at compile time to whatever SQL the Python macro function returns. Test SQL uses macros, enums, and constants available from the test file’s directory under tests/unit/. Public enums and constants available to a model are also available when the test defines __expected__model_name output for that model. Model-private values are not available to tests. See How Visibility Works.

Macro mocking

When a model uses macros that you want to control in tests (e.g. target-specific logic, dynamic SQL generation), you can override their output with __macro__<name> CTEs:
When a __macro__ mock is defined, every call to @country_filter(...) in any model SQL resolved by the test is replaced with the mock value (country_code = 'US'). The macro’s actual Python function is not called, and the arguments are ignored. The mock value must be a single SELECT with one string literal. Use doubled single quotes for quotes within the value (standard SQL escaping). This is useful for:
  • Testing models that use target-specific macros without depending on target config
  • Controlling dynamic SQL generation to produce predictable test inputs
  • Isolating model logic from macro implementation details

Assertions

Unit tests can include __assert__<name> CTEs for property-based checks. An assertion passes if the query returns zero rows - any returned rows are failing examples.
Assertions can be mixed with __expected__ CTEs in the same test, or used on their own. They are useful when the natural check is “no rows should violate this rule” rather than “the output should exactly equal these rows” - for example, duplicate checks, negative-value constraints, or conditional business rules. During sqb test and sqb build, assertion results appear as nested check rows alongside expected comparisons.

Test modes

By default, TEST() runs in model mode - mocking sources/refs and comparing model outputs. Three additional modes let you test reusable logic directly without needing a model chain.

Macro tests

Test macro output by calling the macro in __macro_actual__ and comparing against __macro_expected__:
Macros are compile-time code, so macro tests expand the macro at compile time and compare the results. During sqb build, macro tests run before any model that uses the tested macro.

UDF tests

Test scalar UDFs by calling them in __udf_actual__ and comparing against __udf_expected__:
UDFs are warehouse objects, so the function is created before the test runs. During sqb build, UDF tests run after the function is created but before any model that uses it.

Table function tests

Test table functions by calling them in __table_fn_actual__ and comparing against __table_fn_expected__:
Table function tests run after the function is created. Since table functions are terminal (models cannot depend on them), these tests validate the function independently.

Mode rules

Each mode has strict CTE validation: CTE prefixes from other modes are not allowed. For example, __source__ in a macro test or __macro_actual__ in a model test will produce a clear error pointing you to the right mode. Expected CTEs must not call macros, UDFs, or table functions - they should be independent, inspectable expected data.

Multiple tests per file

A single test file can contain multiple TEST() blocks. Each block must have a unique name:
A file with a single test can omit the name field. Files with multiple tests require names on every block.

Repeating test logic

SQLBuild supports three repetition patterns:

Native independent cases

Declare a typed schema and ordered named cases in one header:
The cases report independently as order status: maps source states [completed], [cancelled], and [pending]. Authored order controls display order. Selecting stg_orders selects every case by default. Add --case to run one named case without changing the test file:
If the case does not exist within the parent/model selection, SQLBuild reports the available case names. Supported scalar types are string, integer, boolean, float, and exact decimal. Decimal values are quoted, such as tax_rate "0.2000", so they never pass through binary float. Boolean and integer remain distinct. Raw SQL and collection parameters are rejected. Nullable parameters use an expanded declaration and are valid where SQL can infer the null type:
Every case provides exactly the declared parameters, and every declaration must appear as @param("name") in the template. SQLBuild rejects missing, extra, incompatible, undeclared, and unused values before execution. Expansion is deterministic:
Values can therefore feed macro arguments, for example @order_fixture(@param("source_status")). Parameters work in model, macro, UDF, and table-function modes and preserve every expected model in multi-model tests. Text and JSON output include parent/case identity and safe typed values. Compile JSON, manifests, DAG checks, and compiled/runtime SQL artifacts retain source, block, case, and content-fingerprint provenance. Changing one case changes that case’s fingerprint without reassigning another case’s stable identity. Scenarios are intentionally not parameterized. Keep one scenario per coherent business world so capture and replay identity remains explicit.

Aggregate SQL case tables

When independent status is unnecessary, a normal SQL case table remains concise:
This produces one aggregate test result. Use native cases when each row must pass or fail independently. Quoted strings in VALUES rows may contain commas, brackets, and parentheses. SQLBuild preserves these literals while extracting test CTEs; use normal SQL quote escaping for embedded quotes.

Test placement

Place unit test files under tests/unit/ in your project directory. SQLBuild discovers all .sql files in this directory recursively.

Running tests

Tests run automatically during sqb build in DAG order, before their target model is materialized. Run tests standalone:
Scope to specific models: