feat(durable-functions): add first-class testing API - #350
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a first-class testing surface to the durable-functions (Azure Functions v4 compat) package via a new durable-functions/testing subpath. This provides ergonomic, typed helpers that run orchestrators against the real in-memory replay engine and run entity batches through the core entity executor, while also documenting the intended testing workflow.
Changes:
- Introduces
durable-functions/testinghelpers:runActivity,runOrchestrator,runEntity, plus an interactivecreateOrchestrationHarness. - Publishes the
./testingsubpath viapackage.jsonexports and validates it in unit tests. - Updates README + changelog to document the new testing workflow and capabilities/limitations.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/azure-functions-durable/src/testing/index.ts | Implements the new testing API (one-shot helpers + interactive in-memory harness + entity batch helper). |
| packages/azure-functions-durable/package.json | Adds the ./testing export for durable-functions/testing. |
| packages/azure-functions-durable/README.md | Documents the new testing entry point and recommended patterns. |
| packages/azure-functions-durable/CHANGELOG.md | Notes the addition of the testing entry point and harness features. |
| packages/azure-functions-durable/test/unit/testing.spec.ts | Adds coverage for activities, orchestrators, harness interactions, timers, and entities. |
| packages/azure-functions-durable/test/unit/compat-exports.spec.ts | Asserts the ./testing subpath export shape in package.json. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/azure-functions-durable/src/testing/index.ts:527
deserializecallsJSON.parsefor any defined string, but protobuf wrapper accessors can materialize emptyStringValuemessages (value "") even when a field is effectively unset. In that case this helper would throwSyntaxError: Unexpected end of JSON input, breaking the testing API for orchestrations/entities that omit output/customStatus. Treat empty strings as absent and wrap parse errors with context.
function deserialize<T>(value: string | undefined): T | undefined {
return value === undefined ? undefined : (JSON.parse(value) as T);
}
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/azure-functions-durable/README.md:103
- The two new bullets under the callHttp behavior note are missing the nested-list indentation, so they render as top-level list items instead of continuing the existing sub-list. This breaks the Markdown structure in the "behavior differences" section.
- **The v3 dummy contexts were replaced by `durable-functions/testing`.** The new helpers run
orchestrators through the real in-memory replay engine and run entity batches directly, without a
Functions host or imports from `@microsoft/durabletask-js`. The entity-lock types above remain
removed. `TaskFailedError` is re-exported from the core SDK (aggregate failures surface as
JS-native `AggregateError`).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cut the `durable-functions/testing` entry point from 19 exported symbols to 4 by reusing what the package and core SDK already ship. - Drop `runActivity` and `TestActivityHandler`. Activities are plain Functions handlers, so they are called directly; only the `InvocationContext` needs a factory (`createActivityContext`). - Drop `OrchestrationHarness`, `createOrchestrationHarness`, and `runEntity`. Interactive scenarios and entities are driven with `TestOrchestrationWorker` / `TestOrchestrationClient` plus the already public `wrapOrchestrator` / `wrapEntity`, which the README now shows. This removes the hand-rolled protobuf entity batch request. - Drop `OrchestrationTestFailure` and `OrchestrationTestStatus` in favour of core `TaskFailureDetails` and the package's `OrchestrationRuntimeStatus`. - Map results through the existing `toDurableOrchestrationStatus`, so a test observes exactly what `client.getStatus()` returns. This also fixes a latent bug: the previous `JSON.parse` of `serializedOutput` throws a `SyntaxError` for a failed instance whose output is a plain error string. - Drop `startAt`, which only accepted past values and was a no-op. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Summary
Adds a
durable-functions/testingsubpath with the smallest surface that covers the common case, reusing what this package and@microsoft/durabletask-jsalready ship instead of building a parallel testing framework.The entry point exports four symbols:
runOrchestrator(handler, options)- runs an orchestrator to a terminal state on the core in-memory backend against fake activitiescreateActivityContext(functionName?)- builds theInvocationContextan activity handler receives at runtimeOrchestratorTestOptions,OrchestrationTestResult- the accompanying typesIt also publishes the testing declarations for both modern package
exportsresolution and classic TypeScriptmoduleResolution: "node"viatypesVersions.Fixes #304
API examples
Activities are ordinary Azure Functions handlers with no durable state, so they are called directly. Only the context needs a factory:
Orchestrators need a replaying worker, so they get a helper:
Design notes
Reuse over duplication.
runOrchestratormaps its result through the package's existingtoDurableOrchestrationStatus, so a test asserts on exactly the valuesclient.getStatus()returns in production. That removes the need for a bespoke status union and payload deserializer. Failures are typed with core'sTaskFailureDetailsand activities with this package's ownActivityHandler, rather than near-identical local copies.That reuse also fixes a real bug: deserializing
serializedOutputwith a bareJSON.parsethrows aSyntaxErrorfor a failed instance, whose output can be a plain non-JSON error string.toDurableOrchestrationStatusalready parses tolerantly.No harness, no
runEntity. External events, terminate, suspend/resume, and entity batches are driven byTestOrchestrationWorker/TestOrchestrationClienttogether with the already publicwrapOrchestrator/wrapEntity. That is roughly ten lines, is the pattern MS Learn's Durable Functions unit-testing guidance points JavaScript users at, and it avoids wrapping every core lifecycle method plus hand-rolling a protobuf entity batch request. The README documents it and a unit test pins it.For comparison, the classic
durable-functionsv3 SDK's entire public testing API was two dummy context classes; its replay helpers were never public.Scope and limitations
mainexposes no virtual clock or timer-advance API, so none is fabricated here; keep test delays short.runOrchestratorhas no forced timeout. It returns only after the orchestration is terminal and the worker has drained, so activity code cannot keep mutating test state afterwards. Arbitrary JavaScript promises cannot be cancelled, so a handler that never settles leaves the helper pending and the test runner's timeout applies.startAtis not offered. The in-memory backend enqueues scheduled starts immediately, so only past values were ever accepted, which is indistinguishable from starting now.Validation
npm run test:unit -w durable-functions(19 suites, 162 tests)npx tsc -p tsconfig.build.json --noEmitfor the packagecompat-exports.spec.tsresolvesdurable-functions/testingtodist/testing/index.d.tsthrough a real TypeScriptNode10/classic resolver check against an extractednpm packtarball