FabricFabric
Testing on Databricks

Local testing

Hermetic Databricks tests with no workspace — DuckDB execution contexts, client-boundary mocks, fixtures, and cross-engine parity.

Status: Available — @fabricorg/databricks-testkit provides the DuckDB context, fixtures, golden files, the mock client, and cross-engine parity suites. The live half of parity runs when DBX_TEST_LIVE=1 is set.

The local tier runs your Databricks-bound logic with no workspace, no credentials, and no network — fast enough for watch mode and every CI run.

DuckDB execution context

import { createDuckDbContext } from '@fabricorg/databricks-testkit'

const ctx = await createDuckDbContext()
await ctx.loadFixture({
  table: 'exposures',
  ndjsonFile: 'fixtures/exposures.ndjson',
})

const rows = await ctx.query(aggregateSql)

DuckDB executes the portable SQL emitted for the local profile. The live profile executes the corresponding Databricks SQL against a warehouse. Keep analytical SQL in a single emitter (the repo's pattern is a shared SQL AST in packages/warehouse with emitDatabricksAggregate / emitDuckDbAggregate) so both engines stay in sync by construction.

Client-boundary mocks

Everything that talks to the Databricks REST API goes through a DatabricksRestClient that accepts an injected fetch/client. Unit tests stub at that boundary and drive multi-poll state machines with sequenced responses:

import { createMockDatabricksClient } from '@fabricorg/databricks-testkit'

const client = createMockDatabricksClient()
client.get
  .mockResolvedValueOnce({ state: { life_cycle_state: 'RUNNING' } })
  .mockResolvedValueOnce({
    state: { life_cycle_state: 'TERMINATED', result_state: 'SUCCESS' },
  })

const run = await jobs.wait(runId, { client })

This tests job/pipeline orchestration logic—retries, timeouts and terminal-state handling—in milliseconds. Both the REST client and OAuth exchange accept an injectable fetchImpl, so deterministic response sequences remain ordinary committed test fixtures rather than recordings that can contain credentials.

Fixtures

Fixtures are declarative and engine-agnostic — the same seed applies to a local DuckDB table or a scratch Unity Catalog schema in live runs:

await ctx.loadFixture({
  table: 'conversions',
  schema: 'subject_id STRING, event_name STRING, value DOUBLE, at TIMESTAMP',
  rows: [
    ['u1', 'purchase', 49.0, '2026-07-01T10:00:00Z'],
    ['u2', 'purchase', 12.5, '2026-07-01T11:30:00Z'],
  ],
})

Cross-engine parity

Golden-result suites prove DuckDB and Databricks agree:

import { defineParitySuite } from '@fabricorg/databricks-testkit/parity'

defineParitySuite({
  name: 'aggregate-attribution',
  sql: emitDatabricksAggregate(query),
  fixtures: [
    {
      table: 'exposures',
      schema: 'experiment_id STRING, subject_id STRING, variant_key STRING',
      ndjsonFile: 'fixtures/exposures.ndjson',
    },
    {
      table: 'conversions',
      schema: 'subject_id STRING, event_name STRING, value DOUBLE',
      ndjsonFile: 'fixtures/conversions.ndjson',
    },
  ],
  golden: 'golden/aggregate-attribution.json',
})

Locally the suite asserts DuckDB output against the golden file. When live credentials are present (see Live artifact checks) it also runs the statement on a real warehouse and asserts both engines match the same golden rows. A change to the golden file is a reviewable, versioned event — exactly like the frozen assignment vectors in packages/testkit.

On this page