Testing and Contracts
Rocky provides compile-time contract validation, local model testing via DuckDB, and a CI pipeline command that combines both. These features catch problems before models reach the warehouse.
Data contracts
Section titled “Data contracts”A data contract is a TOML file that declares expectations about a model’s output schema. The compiler validates inferred schemas against contracts at compile time, catching issues like missing columns, type mismatches, and nullability violations.
Contract format
Section titled “Contract format”Contracts are stored as {model_name}.contract.toml files in a contracts directory:
[[columns]]name = "customer_id"type = "Int64"nullable = falsedescription = "Unique customer identifier"
[[columns]]name = "total_revenue"type = "Decimal"nullable = false
[[columns]]name = "order_count"type = "Int64"nullable = false
[rules]required = ["customer_id", "total_revenue"]protected = ["customer_id"]no_new_nullable = trueColumn constraints
Section titled “Column constraints”Each [[columns]] entry can specify:
| Field | Required | Description |
|---|---|---|
name |
Yes | Column name |
type |
No | Expected Rocky type (Int64, String, Decimal, Timestamp, etc.) |
nullable |
No | If false, the column must be non-nullable |
description |
No | Documentation (not validated, for human readers) |
Type names correspond to RockyType variants: Boolean, Int32, Int64, Float32, Float64, Decimal, String, Binary, Date, Timestamp, TimestampNtz, Array, Map, Struct, Variant.
Schema rules
Section titled “Schema rules”The [rules] section enforces schema-level constraints:
| Rule | Description |
|---|---|
required |
Columns that must exist in the model’s output. Missing required columns produce error E010. |
protected |
Columns that must never be removed. If a protected column disappears from the output, it produces error E013. |
no_new_nullable |
If true, no new nullable columns may be added to the model’s output. |
Diagnostic codes
Section titled “Diagnostic codes”| Code | Severity | Meaning |
|---|---|---|
E010 |
Error | Required column missing from model output |
E011 |
Error | Column type mismatch (contract expects one type, model produces another) |
E012 |
Error | Nullability violation (contract says non-nullable, model says nullable) |
E013 |
Error | Protected column has been removed |
W010 |
Warning | Contract defines a column that is not in the model output (but not required) |
W011 |
Warning | Contract exists for a model that was not found in the project |
When a column has type Unknown (the compiler could not infer its type), type checks against contracts pass without error. This avoids false positives when type information is incomplete.
rocky test
Section titled “rocky test”The rocky test command compiles models and executes them locally using DuckDB, without requiring a warehouse connection. This provides fast feedback during development.
How it works
Section titled “How it works”- Compile. All models are compiled through the full pipeline (load, resolve, semantic graph, type check, contracts).
- Execute locally. Each model’s SQL is executed against an in-memory DuckDB instance. Models run in topological order so upstream models exist before downstream models reference them.
- Validate. If contracts are present, the output schemas are checked. Compilation diagnostics are also reported.
- Report. Pass/fail results are printed for each model.
# Run all testsrocky test --models models/
# Run with contractsrocky test --models models/ --contracts contracts/
# JSON output for CI systemsrocky test --models models/ --output jsonTest output
Section titled “Test output”Testing 12 models...
All 12 models passed
Result: 12 passed, 0 failedOn failure:
Testing 12 models...
x orders_summary -- column 'revenue' type mismatch: expected Decimal, got String x customer_ltv -- required column 'customer_id' missing
Result: 10 passed, 2 failedJSON output
Section titled “JSON output”{ "version": "1.6.0", "command": "test", "total": 12, "passed": 10, "failed": 2, "failures": [ { "name": "orders_summary", "error": "column 'revenue' type mismatch" }, { "name": "customer_ltv", "error": "required column 'customer_id' missing" } ]}Unit tests ([[test]])
Section titled “Unit tests ([[test]])”A unit test feeds a model mocked input rows and asserts on the rows it produces. This is the same approach dbt 1.8 ships as unit tests: you exercise the model’s logic in isolation, against fixtures you control, without touching the warehouse. Rocky runs unit tests on the default rocky test path via DuckDB, alongside the local model-execution check above.
Unit tests live in a model’s .toml sidecar as singular [[test]] blocks. Each block names the test, declares one or more mocked inputs under [[test.given]], and declares the expected output under [test.expect]:
[[test]]name = "high_value_orders"description = "Orders over $100 should be flagged as high value"
[[test.given]]ref = "orders"rows = [ { id = 1, amount = 150.0, status = "completed" }, { id = 2, amount = 50.0, status = "completed" }, { id = 3, amount = 200.0, status = "cancelled" },]
[test.expect]rows = [ { id = 1, amount = 150.0, is_high_value = true }, { id = 3, amount = 200.0, is_high_value = true },]The runner seeds DuckDB with each [[test.given]] fixture as a table named after its ref, executes the model’s compiled SQL against those fixtures, and compares the result to [test.expect].
| Field | Required | Description |
|---|---|---|
name |
Yes | Test name, unique within the model. |
description |
No | Documentation for human readers. |
[[test.given]] |
No | A mocked upstream input. ref is the model or source name to stand in for (matches the model’s from / depends_on references); rows is an inline list of input rows. Repeat the block to mock more than one input. |
[test.expect] |
Yes | The expected output. rows is an inline list of expected rows; ordered is an optional boolean. |
Comparison rules:
- Multiset by default. Rows are compared as a multiset (order does not matter, but duplicate counts do), implemented as
EXCEPT ALLin both directions so a missing row and an unexpected row are both reported. - Ordered comparison. Set
ordered = trueunder[test.expect]to compare rows positionally, in the model’s output order against the declaration order of the expected rows. - Only asserted columns are compared. The comparison uses the columns present in the expected rows. Extra columns in the model’s output are ignored, so you assert on the columns you care about.
- Empty
rowsasserts zero output. An empty[test.expect]rowslist asserts that the model produces no rows for the given inputs.
# Unit tests run automatically on the default test pathrocky test --models models/
# Scope to one modelrocky test --models models/ --model orders_summaryWhen a project declares any [[test]] blocks, rocky test reports a unit-test summary after the model results, and the --output json payload gains a unit_tests object:
{ "version": "1.6.0", "command": "test", "total": 12, "passed": 12, "failed": 0, "failures": [], "unit_tests": { "total": 3, "passed": 2, "failed": 1, "results": [ { "model": "orders_summary", "test": "high_value_orders", "passed": false, "error": "output mismatch: 1 expected row(s) missing, 0 unexpected row(s)" } ] }}A failed unit test also carries a mismatches array of row-level diagnostics (each entry naming a missing, extra, or value-differing row), omitted above for brevity. A unit-test failure fails the rocky test run with a non-zero exit code, the same as a model-execution failure.
Declarative tests ([[tests]])
Section titled “Declarative tests ([[tests]])”Declarative tests are assertions about the data already in your warehouse: not-null columns, uniqueness, accepted values, referential integrity, row-count ranges, and more. They share the assertion vocabulary of pipeline-level data quality checks. See Data quality checks for the full catalog of assertion kinds, severity, and quarantine behavior.
Declarative tests use the plural [[tests]] array in a model’s .toml sidecar. Each entry declares a type, an optional column, an optional severity, an optional filter, and type-specific parameters:
[[tests]]type = "not_null"column = "customer_id"
[[tests]]type = "unique"column = "order_id"
[[tests]]type = "accepted_values"column = "status"values = ["pending", "shipped", "delivered"]severity = "warning"Run them with --declarative. Unlike unit tests, declarative tests execute against the configured warehouse adapter rather than DuckDB, so they need a rocky.toml and a reachable warehouse:
# Run declarative assertions against the warehouserocky test --declarative
# Pick a pipeline when the config defines more than onerocky test --declarative --pipeline silver
# Scope to one modelrocky test --declarative --model orders_summaryEach assertion compiles to a SQL query in the adapter’s dialect, runs against the model’s target table, and reports pass, fail, or error. An assertion with severity = "error" (the default) that fails causes a non-zero exit; severity = "warning" reports without failing the run. The --output json payload carries a declarative summary with per-assertion results and the SQL that ran.
Reusable named tests
Section titled “Reusable named tests”To apply the same assertion across many models, define it once in models/test_definitions.toml and reference it by name with a [[use_test]] block. Inline [[tests]] and [[use_test]] references coexist in a sidecar, and references resolve into ordinary assertions at load. See the Reusable named tests section of the data quality checks page for the full syntax.
[[test]] vs [[tests]]
Section titled “[[test]] vs [[tests]]”The singular and plural keys are two different test mechanisms:
[[test]] (singular) |
[[tests]] (plural) |
|
|---|---|---|
| What it tests | Model logic against mocked inputs | Data already in the warehouse |
| Inputs | [[test.given]] fixtures you supply |
The model’s real target table |
| Executes against | DuckDB, locally | The configured warehouse adapter |
| How to run | rocky test (default path) |
rocky test --declarative |
| Analogous to | dbt 1.8 unit tests | dbt / DQX data tests and assertions |
rocky ci
Section titled “rocky ci”The rocky ci command runs the full CI pipeline: compile + test. It is designed for CI/CD systems and returns a non-zero exit code on failure.
rocky ci --models models/ --contracts contracts/Pipeline
Section titled “Pipeline”- Compile – Run the full compiler (type checking, contract validation)
- Test – Execute all models locally via DuckDB
Both phases must pass for the CI pipeline to succeed.
Output
Section titled “Output”Rocky CI Pipeline
Compile: PASS (12 models) Test: PASS (12 passed, 0 failed)
Exit code: 0Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | All checks passed |
| 1 | Compilation or tests failed (type errors, contract violations, or models that failed to execute locally) |
| 4 | Compiled and tested clean, but advisory warnings were emitted |
JSON output
Section titled “JSON output”{ "version": "1.6.0", "command": "ci", "compile_ok": true, "tests_ok": true, "models_compiled": 12, "tests_passed": 12, "tests_failed": 0, "exit_code": 0, "diagnostics": [], "failures": []}AI-generated tests
Section titled “AI-generated tests”Rocky can generate test assertions from a model’s intent and schema using rocky ai-test. See the AI and Intent page for the full AI workflow.
Each generated assertion is a SQL query that returns 0 rows when the assertion holds:
-- test: orders_summary_no_null_customer_id-- description: customer_id must never be NULLSELECT *FROM warehouse.silver.orders_summaryWHERE customer_id IS NULL-- test: orders_summary_positive_revenue-- description: total_revenue must be non-negativeSELECT *FROM warehouse.silver.orders_summaryWHERE total_revenue < 0Generated tests cover:
- Not-null constraints on key columns
- Grain uniqueness (no duplicate rows for the primary key)
- Value range expectations (non-negative amounts, valid dates)
- Referential integrity (foreign keys exist in parent tables)
Tests are saved to a tests/ directory and can be run alongside contract validation.
Workflow
Section titled “Workflow”A typical development workflow combines contracts, testing, and CI:
- Write a model (SQL or Rocky DSL)
- Write a contract defining the expected output schema
- Run
rocky testlocally to verify everything compiles and executes - Commit and push – CI runs
rocky cito catch regressions - Optionally, run
rocky ai-test --saveto generate additional assertions from intent