Skip to content

feat(durable-functions): add first-class testing API - #350

Merged
wangbill (YunchuWang) merged 6 commits into
mainfrom
yunchuwang-durable-functions-testing
Aug 6, 2026
Merged

feat(durable-functions): add first-class testing API#350
wangbill (YunchuWang) merged 6 commits into
mainfrom
yunchuwang-durable-functions-testing

Conversation

@YunchuWang

@YunchuWang wangbill (YunchuWang) commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Adds a durable-functions/testing subpath with the smallest surface that covers the common case, reusing what this package and @microsoft/durabletask-js already 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 activities
  • createActivityContext(functionName?) - builds the InvocationContext an activity handler receives at runtime
  • OrchestratorTestOptions, OrchestrationTestResult - the accompanying types

It also publishes the testing declarations for both modern package exports resolution and classic TypeScript moduleResolution: "node" via typesVersions.

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:

import { createActivityContext } from "durable-functions/testing";

expect(await sayHello("World", createActivityContext("sayHello"))).toBe("Hello, World!");

Orchestrators need a replaying worker, so they get a helper:

import { OrchestrationRuntimeStatus } from "durable-functions";
import { runOrchestrator } from "durable-functions/testing";

const result = await runOrchestrator<string>(helloOrchestrator, {
  input: "World",
  activities: { sayHello: (name) => `Hello, ${String(name)}!` },
});

expect(result.runtimeStatus).toBe(OrchestrationRuntimeStatus.Completed);
expect(result.output).toBe("Hello, World!");

Design notes

Reuse over duplication. runOrchestrator maps its result through the package's existing toDurableOrchestrationStatus, so a test asserts on exactly the values client.getStatus() returns in production. That removes the need for a bespoke status union and payload deserializer. Failures are typed with core's TaskFailureDetails and activities with this package's own ActivityHandler, rather than near-identical local copies.

That reuse also fixes a real bug: deserializing serializedOutput with a bare JSON.parse throws a SyntaxError for a failed instance, whose output can be a plain non-JSON error string. toDurableOrchestrationStatus already parses tolerantly.

No harness, no runEntity. External events, terminate, suspend/resume, and entity batches are driven by TestOrchestrationWorker / TestOrchestrationClient together with the already public wrapOrchestrator / 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-functions v3 SDK's entire public testing API was two dummy context classes; its replay helpers were never public.

Scope and limitations

  • Orchestrations run on the existing core in-memory replay backend. No fake orchestration context, no duplicated executor logic.
  • Durable timers use real wall-clock delays. main exposes no virtual clock or timer-advance API, so none is fabricated here; keep test delays short.
  • runOrchestrator has 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.
  • startAt is 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 --noEmit for the package
  • ESLint and Prettier on changed files
  • compat-exports.spec.ts resolves durable-functions/testing to dist/testing/index.d.ts through a real TypeScript Node10/classic resolver check against an extracted npm pack tarball

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 5, 2026 16:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/testing helpers: runActivity, runOrchestrator, runEntity, plus an interactive createOrchestrationHarness.
  • Publishes the ./testing subpath via package.json exports 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.

Comment thread packages/azure-functions-durable/src/testing/index.ts Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 5, 2026 17:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • deserialize calls JSON.parse for any defined string, but protobuf wrapper accessors can materialize empty StringValue messages (value "") even when a field is effectively unset. In that case this helper would throw SyntaxError: 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>
Copilot AI review requested due to automatic review settings August 5, 2026 17:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI review requested due to automatic review settings August 5, 2026 18:17
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

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>
@YunchuWang
wangbill (YunchuWang) merged commit 8569498 into main Aug 6, 2026
16 checks passed
@YunchuWang
wangbill (YunchuWang) deleted the yunchuwang-durable-functions-testing branch August 6, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide a first-class testing story for the durable-functions (v4) compat package

3 participants