Testing
Unit-test workflows with an in-memory engine harness, crash/flaky-step injection, and replay assertions — no Postgres, no Redis, no real time.
@dudousxd/nestjs-durable-testing runs a whole workflow in a unit test — in-memory store and
transport, a clock you control, and assertions that read back the recorded state.
pnpm add -D @dudousxd/nestjs-durable-testingA test engine
import { createTestEngine, assertRunStatus, assertOutput } from '@dudousxd/nestjs-durable-testing';
const t = createTestEngine(); // { engine, store, transport, clock, tick }
t.engine.register('checkout', '1', async (ctx) => {
await ctx.step('reserve', () => reserve());
return ctx.step('ship', () => ship());
});
const { runId } = await t.engine.start('checkout', order, 'run1'); // enqueues → { status: 'pending' }
await t.engine.waitForRun(runId); // resolves when the run settles
await assertRunStatus(t.store, 'run1', 'completed');Control time (durable sleep)
tick(ms) advances the clock and resumes any durable sleep that is now due — no waiting:
t.engine.register('digest', '1', async (ctx) => {
await ctx.step('draft', () => draft());
await ctx.sleep('7 days');
await ctx.step('send', () => send());
});
const { runId } = await t.engine.start('digest', {}, 'run1'); // enqueues → { status: 'pending' }
await t.engine.waitForRun(runId); // settles on the durable sleep → suspended
await t.tick(7 * 24 * 60 * 60 * 1000); // the sleep is due → completes
await assertRunStatus(t.store, 'run1', 'completed');Inject crashes & retries
failOnce / failTimes make a step throw before succeeding — to drive retries and resume:
import { failOnce, assertStepAttempts } from '@dudousxd/nestjs-durable-testing';
t.engine.register('wf', '1', async (ctx) =>
ctx.step('charge', failOnce({ ok: true }), { retries: 3 }),
);
const { runId } = await t.engine.start('wf', {}, 'run1');
await t.engine.waitForRun(runId);
await assertStepAttempts(t.store, 'run1', 'charge', 2); // failed once, then succeededAssertions
assertRunStatus, assertOutput, assertStepsRan, assertStepAttempts, and recordedSteps —
all read the store, so they work against any run the engine produced.
Linting for non-determinism
Catch Date.now(), Math.random(), new Date() and crypto.randomUUID() inside a @Workflow run at author time with @dudousxd/nestjs-durable-eslint-plugin — shipped for both ESLint (flat config) and Biome (GritQL plugin).
CLI
Inspect workflow runs and their step timelines from the terminal with `durable inspect`.