Skip to content

BDD with bddHarness

Write your acceptance criteria as Gherkin and run them against a real in-process app — every step dispatches through the actual runtime, not a mock. Nwire runs the scenarios through @amiceli/vitest-cucumber, and the bddHarness({ buildApp }) helper from @nwire/test-kit wires the harness into each scenario for you.

(It also absorbs a lifecycle quirk: BeforeEachScenario fires in the scenario's beforeAll, after the Background describe runs — so booting the app by hand in a step file lands at the wrong time. The helper handles that ordering so you don't have to.)

Install

bash
pnpm add -D @nwire/test-kit @amiceli/vitest-cucumber vitest

@amiceli/vitest-cucumber is a peer dep of test-kit's BDD helpers.

The step file pattern

ts
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
import { expect } from "vitest";
import { bddHarness } from "@nwire/test-kit";
import { buildApp } from "../../app";
import { placeOrder } from "shop";

const feature = await loadFeature("./features/order-placement.feature");

describeFeature(feature, ({ Scenario, Background, AfterEachScenario }) => {
  const ctx = bddHarness({
    buildApp,
    defaultUser: { id: "alice", roles: ["customer"] },
  });

  AfterEachScenario(() => ctx.reset());

  Background(({ Given }) => {
    Given("the platform is up", async () => {
      await ctx.boot();
    });
  });

  Scenario("Alice places an order", ({ When, Then }) => {
    When("she places an order for {string}", async (_ctx, sku: string) => {
      await ctx.scoped().dispatch(placeOrder, { orderId: "o-1", sku });
    });
    Then("the order is recorded", async () => {
      expect(
        ctx.harness!.telemetry.count(
          "event.published",
          (e) => (e.event as { eventName?: string }).eventName === "shop.order-placed",
        ),
      ).toBeGreaterThan(0);
    });
  });
});

The bddHarness API

ts
const ctx = bddHarness({ buildApp, defaultUser?, tenantPrefix? });
MemberWhen to callWhat it does
ctx.boot()At the head of every stepLazy-init the harness if not already booted. Idempotent within a scenario.
ctx.scoped(overrides?)When dispatching/queryingReturns an asUser-pinned scoped harness. Tenant + user are taken from ctx.user / ctx.tenant.
ctx.userBackground/Scenario stepsSwap the pinned user for a step ("Dan is logged in as admin").
ctx.reset()AfterEachScenarioTear down the harness between scenarios.
ctx.harnessAnywhere after boot()Direct access to the underlying Harness for telemetry, idle-wait, etc.
ctx.tenantRead-onlyPer-scenario tenant string ("t-1", "t-2", …). Isolates state when needed.

ctx.boot() is the trick — call it as the first line of every Background step and every first Scenario step. The lazy-init makes the Background and Scenario steps wire to the same harness regardless of vitest-cucumber's hook ordering.

Step-text reuse caveat

vitest-cucumber binds registrations to feature steps by text. If a feature uses the same step pattern twice in one scope (Background or Scenario), each occurrence binds to the first matching registration — the second occurrence reports as "missing". Give each occurrence unique wording in the .feature file:

gherkin
# Will fail — both bind to the first "a station {string}…" registration
Background:
  Given a station "stn_a" exists
  Given a station "stn_b" exists

# Works — unique wording per occurrence
Background:
  Given a station "stn_a" exists
  Given another station "stn_b" exists

Running

Point vitest at your step files. The example layout uses *.steps.ts:

jsonc
// vite.config.ts
{
  test: {
    include: [
      "**/*.{test,spec}.?(c|m)[jt]s?(x)",
      "**/*.steps.?(c|m)[jt]s?(x)",
    ],
  },
}
json
// package.json
{
  "scripts": {
    "test:bdd": "vitest run apps/main/__tests__/steps"
  }
}

Real-world reference

The station-management example carries four full Gherkin suites (station-crud.feature, wash-recording.feature, wash-webhook.feature, idempotency-and-flow.feature) running through the in-process harness, plus a real Koa + endpoint stack per-scenario for the HMAC-signed webhook gate. 173 scenarios, all through bddHarness.

See: examples/station-management/apps/main/__tests__/steps/*.steps.ts.

See also

  • Testing — unit + integration tests via harness({app}).

MIT licensed.