FabricFabricExperiments
Testing on Databricks

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

LayerRunsBest forGate
UnitEvery changeREST orchestration, retries, terminal states with an injected mock clientRequired
Local BDDEvery changeSQL logic, fixtures, contracts, domain rules on DuckDBRequired
Cross-engine parityPull request or nightlyDuckDB and Photon compared to one committed golden resultRequired for analytical SQL
Live BDDNightly or releaseHuman-readable Jobs, Lakeflow, UC, Volumes, Lakebase, notebook and dbt outcomesRequired selectively
Artifact checksNightlyResource health, bindings, performance, recovery, rotation, backup and rollbackRequired after observation
Browser-to-StudioStaging promotionCloudflare ingestion through Delta and authenticated analytics UIPromotion 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

WorkloadLocal contractLive assertion
SQL and DeltaFixtures, golden rows, schema contractsPhoton parity, table detail, performance
Unity CatalogGenerated grant statementsPositive and negative access with a restricted identity
JobsSubmit/poll state machine with mock responsesRun reaches SUCCESS within a budget
NotebooksPure logic extracted into testable modulesExisting-cluster or serverless submit
Lakeflow / Auto LoaderFixture and rescue-column contractUpload → refresh → Delta rows, no rescued data
VolumesPath and client-boundary testsUpload, download, compare, delete
LakebaseCredential/provider unit testsOAuth exchange, query, refresh, pool swap, query
dbtSource/model contractServerless dbt build against a scratch schema
Databricks AppsBundle validationApp 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:

  1. A bootstrap identity creates the run-scoped schema or branch and grants the minimum access.
  2. A workload identity runs every positive assertion.
  3. A restricted secondary identity proves denied access remains denied.
  4. 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 build

Recommended scheduled or release jobs:

create scratch environment
  → live BDD (serial)
  → required artifact checks
  → upload JUnit/HTML/JSON evidence
  → destroy scratch environment, even after failure

Use 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.

Advanced reference map

On this page