Summary
The new durable-functions v4 compat package (added in #282, built on the @microsoft/durabletask-js gRPC core) currently ships no first-class testing story. This is a regression in developer experience versus v3 (azure-functions-durable-js), where DummyOrchestrationContext / DummyEntityContext let you unit‑test an orchestrator/entity in one line with no host, no Docker, no network.
This issue proposes a durable-functions/testing subpath entry point that gives customers an elegant, Functions‑native way to test v4 orchestrators, entities, and activities — without importing core internals or knowing about wrapOrchestrator.
Filed as a fast‑follow to #282 (does not need to block that PR).
Background: how v3 testing worked (the bar we are regressing from)
v3 exposed fake contexts specifically for the Azure Functions programming model:
azure-functions-durable-js — src/util/testingUtils.ts: DummyOrchestrationContext extends InvocationContext, DummyEntityContext<T>.
- Real usage:
test/integration/orchestrator-spec.ts, test/integration/entity-spec.ts.
Orchestrator test (v3):
const orchestrator = TestOrchestrations.SayHelloInline;
const mockContext = new DummyOrchestrationContext();
const input = new DurableOrchestrationInput("", TestHistories.GetOrchestratorStart(...), name);
const result = await orchestrator(input, mockContext);
expect(result).to.deep.equal(new OrchestratorState({ isDone: true, output: "Hello, World!", ... }, true));
Entity test (v3):
const mockContext = new DummyEntityContext<string>();
const result = await entity(testData.input, mockContext);
entityStateMatchesExpected(result, testData.output);
One new Dummy*Context() + a hand-built history/batch + a single synchronous call. That is the ergonomic bar.
Current state in v4 (the problem)
-
The compat package exports zero testing utilities. packages/azure-functions-durable/src/index.ts only re-exports runtime types + DurableFunctionsWorker. There is no Dummy*, no Test*, no InMemory*.
-
The only way to drive a wrapped orchestrator today is to reach into core internals, exactly as the package's own e2e tests do — see packages/azure-functions-durable/test/unit/orchestration-context.spec.ts (describe("wrapOrchestrator end-to-end (real core executor)")):
import { InMemoryOrchestrationBackend, TestOrchestrationWorker, TestOrchestrationClient } from "@microsoft/durabletask-js";
import { wrapOrchestrator } from "../../src/orchestration-context"; // internal path
const backend = new InMemoryOrchestrationBackend();
const worker = new TestOrchestrationWorker(backend);
worker.addNamedActivity("echo", echo);
worker.addNamedOrchestrator("orch", wrapOrchestrator(myOrch)); // customer must know to wrap
await worker.start();
const client = new TestOrchestrationClient(backend);
const id = await client.scheduleNewOrchestration("orch", "IN");
const state = await client.waitForOrchestrationCompletion(id, true, 10);
expect(state?.serializedOutput).toBe(JSON.stringify("classic-done:echo:IN")); // manual (de)serialization
Four DX problems for customers:
- Imports from the
@microsoft/durabletask-js core package, not from durable-functions.
- Requires knowing about
wrapOrchestrator — an internal bridge concept.
- 5+ lines of backend/worker/client boilerplate per test.
- No entity story at all.
entity-context.spec.ts can only white-box entities via mocked EntityFactory / TaskEntityOperation; there is no DummyEntityContext equivalent.
Design constraint: why orchestrator can't be a v3-style drop-in (and entity can)
Orchestrator. v3's DummyContext worked because the v3 SDK is the replay engine and history is an input parameter (TaskOrchestrationExecutor.execute(context, history[], fn) iterates the array and drives the generator via .next()/.throw()). v4 has no "history array in → run synchronously to completion" seam: history lives inside InMemoryOrchestrationBackend and is advanced by the engine across multiple turns. So the honest v4 replacement is a real in-memory engine run, not a fake context. We should not resurrect a synthetic single-shot DummyOrchestrationContext for orchestrators — it would misrepresent the execution model.
Entity. Entities are request/response batch processing, structurally identical to v3's GetStringStoreBatch. This can be a synchronous single-shot call, so we can and should restore v3-level ergonomics here (Layer 3 below).
Proposed solution: a durable-functions/testing subpath
Add a dedicated testing entry point so customers speak only the Functions model — no core imports, no wrapOrchestrator knowledge. Everything below is a thin wrapper over already-existing building blocks (wrapOrchestrator, wrapEntity + ClassicEntityAdapter, and core InMemoryOrchestrationBackend / TestOrchestrationWorker / TestOrchestrationClient). No core changes required.
Layer 1 — one-shot helpers (covers ~80% of tests)
import { test } from "durable-functions/testing";
const r = await test.runOrchestrator(myOrchestrator, {
input: "World",
activities: { SayHello: (ctx, name) => `Hello, ${name}!` }, // stub activities inline
});
expect(r.status).toBe("Completed");
expect(r.output).toBe("Hello, World!"); // already deserialized — no manual JSON.parse
Layer 2 — harness (for interactive cases: raise event / advance timer / terminate)
const h = test.createOrchestrationHarness();
h.registerOrchestrator("approval", approvalOrch);
h.registerActivity("notify", async () => "sent");
const run = await h.start("approval", { input: {...} });
await run.raiseEvent("Approved", true);
await run.waitForCompletion();
expect(run.output).toEqual({...});
expect(run.actionsInclude("notify")).toBe(true);
Layer 3 — entity (restore v3-style synchronous ergonomics)
const r = await test.runEntity(counterEntity, {
initialState: 0,
operations: [{ name: "add", input: 5 }, { name: "get" }],
});
expect(r.state).toBe(5);
expect(r.results).toEqual([undefined, 5]);
Internals: wrapEntity(counterEntity) → build a TaskEntityOperation[] batch → run once through ClassicEntityAdapter → collect { state, results }. This closes the entity gap left by dropping DummyEntityContext.
Packaging
package.json currently exposes only ".". Add a subpath:
Keeping it on a subpath means the testing helpers (and their transitive dev surface) never leak into the runtime import graph.
Priority / scope
| Capability |
Reuses (existing) |
New code |
Value |
Priority |
test.runActivity(fn, input) |
none (direct call) |
tiny |
med |
P1 |
test.runOrchestrator (Layer 1) |
wrapOrchestrator + core testing |
small wrapper |
high |
P0 |
test.createOrchestrationHarness (Layer 2) |
+ client event/terminate APIs |
medium |
high |
P1 |
test.runEntity (Layer 3) |
wrapEntity + ClassicEntityAdapter + batch driver |
medium |
high (fills entity gap) |
P0 |
durable-functions/testing subpath export |
package.json exports |
tiny |
high |
P0 |
None of these require changes to @microsoft/durabletask-js core.
Interim mitigation (cheap, could even land in #282)
Before the full testing subpath exists, re-export the core testing primitives + the wrap helpers from the compat package and document a "How to test" section in the README, so customers at least stop importing from the core package and internal paths:
- re-export
InMemoryOrchestrationBackend, TestOrchestrationWorker, TestOrchestrationClient (from core) and wrapOrchestrator / wrapEntity.
- README: one worked orchestrator example + one entity example.
Non-goals
- No synthetic single-shot
DummyOrchestrationContext for orchestrators. It cannot faithfully model v4's multi-turn engine execution; the in-memory engine run is the correct replacement.
- No changes to
@microsoft/durabletask-js core testing APIs.
Notes / corrections
- Core exposes exactly three testing primitives (
packages/durabletask-js/src/testing/index.ts): InMemoryOrchestrationBackend, TestOrchestrationClient, TestOrchestrationWorker. There is no TestEntityWorker — entity end-to-end testing is the real gap this issue's Layer 3 addresses.
- Reference for the intended orchestrator pattern:
examples/azure-managed/unit-testing/index.ts.
References
Summary
The new
durable-functionsv4 compat package (added in #282, built on the@microsoft/durabletask-jsgRPC core) currently ships no first-class testing story. This is a regression in developer experience versus v3 (azure-functions-durable-js), whereDummyOrchestrationContext/DummyEntityContextlet you unit‑test an orchestrator/entity in one line with no host, no Docker, no network.This issue proposes a
durable-functions/testingsubpath entry point that gives customers an elegant, Functions‑native way to test v4 orchestrators, entities, and activities — without importing core internals or knowing aboutwrapOrchestrator.Filed as a fast‑follow to #282 (does not need to block that PR).
Background: how v3 testing worked (the bar we are regressing from)
v3 exposed fake contexts specifically for the Azure Functions programming model:
azure-functions-durable-js—src/util/testingUtils.ts:DummyOrchestrationContext extends InvocationContext,DummyEntityContext<T>.test/integration/orchestrator-spec.ts,test/integration/entity-spec.ts.Orchestrator test (v3):
Entity test (v3):
One
new Dummy*Context()+ a hand-built history/batch + a single synchronous call. That is the ergonomic bar.Current state in v4 (the problem)
The compat package exports zero testing utilities.
packages/azure-functions-durable/src/index.tsonly re-exports runtime types +DurableFunctionsWorker. There is noDummy*, noTest*, noInMemory*.The only way to drive a wrapped orchestrator today is to reach into core internals, exactly as the package's own e2e tests do — see
packages/azure-functions-durable/test/unit/orchestration-context.spec.ts(describe("wrapOrchestrator end-to-end (real core executor)")):Four DX problems for customers:
@microsoft/durabletask-jscore package, not fromdurable-functions.wrapOrchestrator— an internal bridge concept.entity-context.spec.tscan only white-box entities via mockedEntityFactory/TaskEntityOperation; there is noDummyEntityContextequivalent.Design constraint: why orchestrator can't be a v3-style drop-in (and entity can)
Orchestrator. v3's
DummyContextworked because the v3 SDK is the replay engine and history is an input parameter (TaskOrchestrationExecutor.execute(context, history[], fn)iterates the array and drives the generator via.next()/.throw()). v4 has no "history array in → run synchronously to completion" seam: history lives insideInMemoryOrchestrationBackendand is advanced by the engine across multiple turns. So the honest v4 replacement is a real in-memory engine run, not a fake context. We should not resurrect a synthetic single-shotDummyOrchestrationContextfor orchestrators — it would misrepresent the execution model.Entity. Entities are request/response batch processing, structurally identical to v3's
GetStringStoreBatch. This can be a synchronous single-shot call, so we can and should restore v3-level ergonomics here (Layer 3 below).Proposed solution: a
durable-functions/testingsubpathAdd a dedicated testing entry point so customers speak only the Functions model — no core imports, no
wrapOrchestratorknowledge. Everything below is a thin wrapper over already-existing building blocks (wrapOrchestrator,wrapEntity+ClassicEntityAdapter, and coreInMemoryOrchestrationBackend/TestOrchestrationWorker/TestOrchestrationClient). No core changes required.Layer 1 — one-shot helpers (covers ~80% of tests)
Layer 2 — harness (for interactive cases: raise event / advance timer / terminate)
Layer 3 — entity (restore v3-style synchronous ergonomics)
Internals:
wrapEntity(counterEntity)→ build aTaskEntityOperation[]batch → run once throughClassicEntityAdapter→ collect{ state, results }. This closes the entity gap left by droppingDummyEntityContext.Packaging
package.jsoncurrently exposes only".". Add a subpath:Keeping it on a subpath means the testing helpers (and their transitive dev surface) never leak into the runtime import graph.
Priority / scope
test.runActivity(fn, input)test.runOrchestrator(Layer 1)wrapOrchestrator+ core testingtest.createOrchestrationHarness(Layer 2)test.runEntity(Layer 3)wrapEntity+ClassicEntityAdapter+ batch driverdurable-functions/testingsubpath exportpackage.jsonexportsNone of these require changes to
@microsoft/durabletask-jscore.Interim mitigation (cheap, could even land in #282)
Before the full
testingsubpath exists, re-export the core testing primitives + the wrap helpers from the compat package and document a "How to test" section in the README, so customers at least stop importing from the core package and internal paths:InMemoryOrchestrationBackend,TestOrchestrationWorker,TestOrchestrationClient(from core) andwrapOrchestrator/wrapEntity.Non-goals
DummyOrchestrationContextfor orchestrators. It cannot faithfully model v4's multi-turn engine execution; the in-memory engine run is the correct replacement.@microsoft/durabletask-jscore testing APIs.Notes / corrections
packages/durabletask-js/src/testing/index.ts):InMemoryOrchestrationBackend,TestOrchestrationClient,TestOrchestrationWorker. There is noTestEntityWorker— entity end-to-end testing is the real gap this issue's Layer 3 addresses.examples/azure-managed/unit-testing/index.ts.References
durable-functions@4.0.0— Azure Functions Durable provider on the gRPC core (+ core host helpers, E2E CI, and release pipeline) #282 (adds thedurable-functionscompat package)packages/azure-functions-durable/src/orchestration-context.ts—wrapOrchestrator(handler: TOrchestrator | ClassicOrchestrator)packages/azure-functions-durable/src/entity-context.ts—wrapEntity(...),ClassicEntityAdapter implements ITaskEntitypackages/azure-functions-durable/test/unit/orchestration-context.spec.ts— current e2e patternpackages/durabletask-js/src/testing/*— core in-memory testing primitives