Advanced Databricks BDD
Design production suites with custom TypeScript steps, live checks, ephemeral resources, governance, evidence, resilience, and CI promotion gates.
This playbook is for platform engineers who already completed the BDD quickstart. It explains how to turn readable features into a production Databricks validation system without coupling every scenario to expensive live infrastructure.
Build a layered suite
| Layer | Runs | Best for | Gate |
|---|---|---|---|
| Unit | Every change | REST orchestration, retries, terminal states with an injected mock client | Required |
| Local BDD | Every change | SQL logic, fixtures, contracts, domain rules on DuckDB | Required |
| Cross-engine parity | Pull request or nightly | DuckDB and Photon compared to one committed golden result | Required for analytical SQL |
| Live BDD | Nightly or release | Human-readable Jobs, Lakeflow, UC, Volumes, Lakebase, notebook and dbt outcomes | Required selectively |
| Artifact checks | Nightly | Resource health, bindings, performance, recovery, rotation, backup and rollback | Required after observation |
| Browser-to-Studio | Staging promotion | Cloudflare ingestion through Delta and authenticated analytics UI | Promotion gate |
Keep most business logic in local BDD. Use live tests for platform behavior that DuckDB cannot prove: identity, grants, engine dialect, managed-resource lifecycle, network boundaries, credentials, and recovery.
Add domain language in TypeScript
Feature authors should speak in product terms. Implement those terms once over the Fabric World instead of duplicating SQL across feature files:
import { Then, When } from '@cucumber/cucumber'
import type { DatabricksWorld } from '@fabricorg/databricks-bdd'
When(
'I calculate the conversion rate for {string}',
async function (this: DatabricksWorld, table: string) {
const ctx = await this.context()
this.lastRows = await ctx.query(
`SELECT variant_key, SUM(conversions) / SUM(exposures) AS rate
FROM ${table}
GROUP BY variant_key`,
)
},
)
Then(
'variant {string} has conversion rate {float}',
function (this: DatabricksWorld, variant: string, expected: number) {
const row = this.lastRows?.find((candidate) => candidate.variant_key === variant)
if (!row || Math.abs(Number(row.rate) - expected) > 0.0001) {
throw new Error(`Expected ${variant} rate ${expected}; received ${String(row?.rate)}`)
}
},
)Import domain files after @fabricorg/databricks-bdd/steps in the support
entrypoint. Declare reusable behavior with defineAction() and call it through
this.runAction() rather than invoking textual steps from other steps; this
keeps types, failure locations, unit tests, and the generated catalog precise.
Manage state and cleanup
DatabricksWorld is scenario-scoped. It lazily creates one execution context,
records the last rows, SQL, error, statement ID, job run, and pipeline update,
and closes resources in the After hook.
For user state, call setState('run' | 'feature' | 'scenario', key, value).
getState() and requireState() resolve scenario values first, then feature,
then run. This provides Behave-style layered context without untyped property
mutation. Values that own resources should be paired with runFixture(),
featureFixture(), or addCleanup() at the same scope.
Register any extra resource in the reverse-order cleanup stack:
this.addCleanup(async () => {
await deleteTemporaryObject()
})Use ephemeral environments for run-scoped Unity Catalog schemas and Lakebase branches. Use scenario prefixes for Volume files. Never make a test reliable by sharing a mutable, long-lived fixture table.
Test each Databricks workload at the right boundary
| Workload | Local contract | Live assertion |
|---|---|---|
| SQL and Delta | Fixtures, golden rows, schema contracts | Photon parity, table detail, performance |
| Unity Catalog | Generated grant statements | Positive and negative access with a restricted identity |
| Jobs | Submit/poll state machine with mock responses | Run reaches SUCCESS within a budget |
| Notebooks | Pure logic extracted into testable modules | Existing-cluster or serverless submit |
| Lakeflow / Auto Loader | Fixture and rescue-column contract | Upload → refresh → Delta rows, no rescued data |
| Volumes | Path and client-boundary tests | Upload, download, compare, delete |
| Lakebase | Credential/provider unit tests | OAuth exchange, query, refresh, pool swap, query |
| dbt | Source/model contract | Serverless dbt build against a scratch schema |
| Databricks Apps | Bundle validation | App running, resource bindings, authenticated health |
The artifact coverage guide gives assertion ideas for each surface. The step catalog lists the exact shipped Gherkin wording.
Compose a required live suite
Use checks for operational assertions that do not need Gherkin narration:
import {
appHealthCheck,
backupRestoreCheck,
defineLiveSuite,
failureRecoveryCheck,
performanceBudgetCheck,
restrictedPrincipalCheck,
rollbackCheck,
secretRotationCheck,
serverlessComputeCheck,
sqlRoundTripCheck,
} from '@fabricorg/databricks-testkit'
const suite = defineLiveSuite({
required: [
'restricted-principal',
'sql',
'azure-serverless',
'app',
'performance',
'failure-recovery',
'secret-rotation',
'backup-restore',
'rollback',
],
checks: [
restrictedPrincipalCheck(),
sqlRoundTripCheck(),
serverlessComputeCheck(),
appHealthCheck(),
performanceBudgetCheck(),
failureRecoveryCheck(),
secretRotationCheck(),
backupRestoreCheck(),
rollbackCheck(),
],
evidencePath: 'reports/databricks-live-evidence.json',
junitPath: 'reports/databricks-live-junit.xml',
})
await suite.run()Each check reports pass, fail, or not-configured. Only IDs in required
gate the process. Start a new check as optional, observe it across normal
workspace load, then promote it to required. See
Live artifact checks for every built-in check and its
configuration.
Add a product-specific live check
import { customLiveCheck } from '@fabricorg/databricks-testkit'
const featureTableFreshness = customLiveCheck(
'feature-table-freshness',
'Feature table was updated within the last hour',
async ({ warehouse }) => {
const rows = await warehouse.query(
'SELECT MAX(updated_at) AS updated_at FROM fx_test.scenarios.features',
)
const updatedAt = new Date(String(rows[0]?.updated_at))
const ageMs = Date.now() - updatedAt.getTime()
if (!Number.isFinite(ageMs) || ageMs > 3_600_000) {
throw new Error(`Feature table age is ${ageMs} ms`)
}
return { updatedAt: updatedAt.toISOString(), ageMs }
},
)Returned details become secret-redacted evidence. Errors become failed checks; do not return a false-like value to represent failure.
Treat identity as test data
Use separate credentials for provisioning and assertions:
- A bootstrap identity creates the run-scoped schema or branch and grants the minimum access.
- A workload identity runs every positive assertion.
- A restricted secondary identity proves denied access remains denied.
- Bootstrap credentials return only for unconditional teardown.
The service principal {string} with grants: step makes least privilege an
executable scenario. Never run the required suite as a workspace administrator;
restrictedPrincipalCheck() fails when the configured workload identity was
not selected.
Evidence and failure diagnosis
Keep all four outputs:
- JUnit XML for CI annotations and history.
- HTML for humans reviewing scenarios.
- Rerun manifests for focused retries.
- Fabric JSON evidence for audit systems and cross-run analysis.
On a failed step, Fabric attaches redacted SQL, a Statement Execution ID, and
captured output. Use DBX_TEST_CAPTURE_OUTPUT=passthrough for an interactive
debug run and off only when another logger already owns capture. Do not print
tokens or secrets; redaction is a final safety layer, not permission to log
credentials.
CI policy
Recommended pull-request jobs:
type-check → unit tests → local BDD → parity/golden checks → package buildRecommended scheduled or release jobs:
create scratch environment
→ live BDD (serial)
→ required artifact checks
→ upload JUnit/HTML/JSON evidence
→ destroy scratch environment, even after failureUse workload identity federation or OAuth M2M instead of long-lived personal
tokens. Protect the live job with environment approval when it can create
classic compute or refresh a pipeline. Set explicit timeouts and cost controls:
warehouse auto-stop, single-node classic probes, per-run schemas, and teardown
under if: always().
Promotion and certification
A single green run proves reachability, not operational stability. Fabric's own staging policy requires seven consecutive scheduled green nights before promotion. Track check version, target cloud, region, DBR/runtime, warehouse mode, identity, start/end time, and evidence digest so a certification can be reproduced.
Azure serverless and one Azure classic-compute configuration are currently certified. AWS and GCP are not claimed as certified targets until their identity, node-type, external-storage, and complete live paths are exercised. See the compatibility matrix for the precise boundary.