Testing

What you can test locally today — build validation, route handlers under the test runtime, and observation mode for the rest.

Extensions have three verification layers today. Know which one covers what, because they’re not interchangeable.

1. backfill build — static validation

backfill build catches the whole class of wiring mistakes before anything is uploaded: manifest parse errors, unknown entities in permissions or hook paths, missing default exports, undeclared HTTP hosts, invalid custom-field declarations, oversized bundles. Run it as your compile step; it’s fast and its errors name the file and the fix.

2. Unit tests — route handlers and pure logic

The test runtime in @backfill-io/sdk/testing installs in-memory implementations of the standard library (Http, Settings, Secrets, Log, Entity, sync, ingest, Crypto, verify, OAuth) and can invoke route handlers directly. It’s documented on the connector testing page, and despite the name it exercises plain extension API routes the same way — skip the connector-specific options and assert on runtime.logs, runtime.http.calls, and your handler’s response:

import { test } from "node:test";
import assert from "node:assert/strict";
import { connectorTestRuntime } from "@backfill-io/sdk/testing";
import { GET } from "../src/api/summary";

test("summary route reads settings", async () => {
  const runtime = connectorTestRuntime({
    settings: { region: "us-east" },
  });

  const response = await runtime.call(GET, { searchParams: {} });
  assert.equal(response.status, 200);
});

Business logic that doesn’t touch the standard library — payload mapping, calculations, date math — needs no runtime at all. Keep it in plain functions under src/lib/ and test it directly; this is where most of your test coverage should live.

3. Hooks — observation mode is the test bed

There is currently no local runtime for hooks: a hook’s HookContext (the entity snapshot, operation, actor) is produced by the platform, and the SDK does not yet ship a mock for it. Structure hooks as thin adapters — extract the context fields, call a tested src/lib/ function, act on the result — so the untested surface stays a few lines.

Then verify the wiring against real traffic without risk:

backfill deploy --observe   # run against live events, effects intercepted
backfill logs --follow      # watch executions + would-have-happened effects
backfill trigger            # no args: lists the deployed hooks and their ids
backfill trigger --hook <id> --data @fixtures/invoice.json
backfill promote            # flip to live once the observed effects look right

In observation mode every write your hook would perform (entity mutations, ingests, outbound HTTP) is recorded as an intercepted effect in the logs instead of committed — a deploy-time dry run. backfill trigger invokes a hook on demand with a fixture payload so you don’t have to wait for a real event.

Jobs

Jobs are plain exported functions, so the split is the same as hooks: test the logic as pure functions; use backfill job run <name> against an observation deploy to verify the wiring and schedule registration.