You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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 pathconstbackend=newInMemoryOrchestrationBackend();constworker=newTestOrchestrationWorker(backend);worker.addNamedActivity("echo",echo);worker.addNamedOrchestrator("orch",wrapOrchestrator(myOrch));// customer must know to wrapawaitworker.start();constclient=newTestOrchestrationClient(backend);constid=awaitclient.scheduleNewOrchestration("orch","IN");conststate=awaitclient.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.
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.jsonexports
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 noTestEntityWorker — 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.
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-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