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:
npm install @ironflow/nodeImport from the /test subpath:
import { createTestClient } from "@ironflow/node/test";The test client lives in the ironflowtest package:
import "github.com/sahina/ironflow/sdk/go/ironflow/ironflowtest"How It Works
The test client replaces the durable step execution engine with a synchronous mock layer. When you call emit(), the client:
- Resolves the function triggered by the event.
- Executes the handler synchronously.
- Replaces
step.run()calls with your registered mocks. - Resolves
step.sleep()andstep.waitForEvent()immediately using pre-registered data. - Returns a
TestRunobject 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" }); });});func TestMyWorkflow(t *testing.T) { tc := ironflowtest.NewClient(t, ironflowtest.Config{ Functions: []ironflow.Function{MyWorkflow}, })
// 1. Mock internal steps tc.MockStep("validate-order", func() (any, error) { return map[string]bool{"valid": true}, nil })
// 2. Execute run := tc.Emit(t, "order.placed", map[string]string{"orderId": "ord_1"})
// 3. Assert if run.Status != "completed" { t.Errorf("expected completed, got %s", run.Status) }}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
- Isolation: Use a fresh
createTestClientfor every test case to ensure no mock or event leakage. - Determinism: Parallel branches (
step.parallel) run sequentially in test mode to ensure your assertions are stable. - Mock Depth: You only need to mock
step.runandstep.invoke. Other primitives likestep.sleepare handled automatically.