Skip to content

Workflow Testing

Ironflow provides an in-memory test client that allows you to unit test your functions without a running server, NATS, or a database. Tests run instantly and deterministically.


Installation

The test client is included in @ironflow/node:

Terminal window
npm install @ironflow/node

Import from the /test subpath:

import { createTestClient } from "@ironflow/node/test";

How It Works

The test client replaces the durable step execution engine with a synchronous mock layer. When you call emit(), the client:

  1. Resolves the function triggered by the event.
  2. Executes the handler synchronously.
  3. Replaces step.run() calls with your registered mocks.
  4. Resolves step.sleep() and step.waitForEvent() immediately using pre-registered data.
  5. Returns a TestRun object containing the final output and an audit log of all executed steps.

Quick Start

import { describe, it, expect } from "vitest";
import { createTestClient } from "@ironflow/node/test";
import { myWorkflow } from "./workflows";
describe("myWorkflow", () => {
it("processes a successful checkout", async () => {
const t = createTestClient({ functions: [myWorkflow] });
// 1. Mock the internal steps
t.mockStep("validate-order", () => ({ valid: true }));
t.mockStep("charge-card", () => ({ txId: "tx_123" }));
// 2. Execute the function
const run = await t.emit("order.placed", { orderId: "ord_1" });
// 3. Assert results
expect(run.status).toBe("completed");
expect(run.output.success).toBe(true);
expect(run.stepOutput("charge-card")).toEqual({ txId: "tx_123" });
});
});

Testing Advanced Patterns

Testing step.waitForEvent

To test a workflow that pauses for an event, you must “prime” the test client with the expected event before emitting the trigger.

it("handles approval flow", async () => {
const t = createTestClient({ functions: [approvalWorkflow] });
// Pre-register the event that waitForEvent will find
t.sendEvent("order.approved", { orderId: "ord_1", approvedBy: "alice" });
const run = await t.emit("order.placed", { orderId: "ord_1" });
expect(run.status).toBe("completed");
});

Testing Sagas (Compensations)

The test client automatically tracks which compensations were registered and executes them in reverse order if a terminal failure occurs.

it("runs compensations on failure", async () => {
const t = createTestClient({ functions: [orderSaga] });
t.mockStep("charge", () => ({ id: "ch_1" }));
t.mockStep("ship", () => { throw new NonRetryableError("Out of stock"); }); // NonRetryableError from "@ironflow/node"
const run = await t.emit("order.placed", {});
expect(run.status).toBe("failed");
// Verify the 'charge' was compensated
expect(run.compensationsRan).toContain("charge");
});

Best Practices

  1. Isolation: Use a fresh createTestClient for every test case to ensure no mock or event leakage.
  2. Determinism: Parallel branches (step.parallel) run sequentially in test mode to ensure your assertions are stable.
  3. Mock Depth: You only need to mock step.run and step.invoke. Other primitives like step.sleep are handled automatically.