From 098c475316470b1e12fbfc0872a3b0aff11b0970 Mon Sep 17 00:00:00 2001 From: Olga Silva Date: Mon, 1 Sep 2025 11:38:45 +0100 Subject: [PATCH 1/8] Add Workflows test support APIs --- .../testing/vitest-integration/test-apis.mdx | 93 +++++++++++++++++++ .../docs/workflows/build/test-workflows.mdx | 9 ++ 2 files changed, 102 insertions(+) create mode 100644 src/content/docs/workflows/build/test-workflows.mdx diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index 7172177592b226c..9b2fd9336a65dc9 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -8,6 +8,8 @@ description: Runtime helpers for writing tests, exported from the `cloudflare:te --- +import { InlineBadge } from '~/components'; + The Workers Vitest integration provides runtime helpers for writing tests in the `cloudflare:test` module. The `cloudflare:test` module is provided by the `@cloudflare/vitest-pool-workers` package, but can only be imported from test files that execute in the Workers runtime. ## `cloudflare:test` module definition @@ -258,3 +260,94 @@ The Workers Vitest integration provides runtime helpers for writing tests in the * Applies all un-applied [D1 migrations](/d1/reference/migrations/) stored in the `migrations` array to database `db`, recording migrations state in the `migrationsTableName` table. `migrationsTableName` defaults to `d1_migrations`. Call the [`readD1Migrations()`](/workers/testing/vitest-integration/configuration/#readd1migrationsmigrationspath) function from the `@cloudflare/vitest-pool-workers/config` package inside Node.js to get the `migrations` array. Refer to the [D1 recipe](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/d1) for an example project using migrations. + +### Workflows + + + +:::caution[Workflows currently require `isolatedStorage` to be disabled] + +To test projects that use Workflows, you **must** set `isolatedStorage` to `false` in your `vitest.config.ts` file. + +::: + +* `introspectWorkflowInstance(workflow: Workflow, instanceId: string)`: Promise\ + * Creates an **introspector** for a specific Workflow instance, used to **modify** its behavior, **await** outcomes, and **clean up** its state during tests. This is the primary entry point for testing individual Workflow instances with a known ID. +
+ + ```ts + import { env, introspectWorkflowInstance } from "cloudflare:test"; + + it("should disable all sleeps, mock an event and complete", async () => { + // 1. CONFIGURATION + const instance = await introspectWorkflowInstance(env.MY_WORKFLOW, "123456"); + await instance.modify(async (m) => { + await m.disableSleeps(); + await m.mockEvent({ type: "user-approval" }, { approval: true }); + }); + + // 2. EXECUTION + await env.MY_WORKFLOW.create({ id: "123456" }); + + // 3. ASSERTION + await instance.waitForStatus("complete"); + + // 4. CLEANUP + await instance.cleanUp(); + }); + ``` + * The returned `WorkflowInstanceIntrospector` object has the following methods: + * `modify(fn: (m: WorkflowInstanceModifier) => Promise)`: Applies modifications to the Workflow instance's behavior. + * `waitForStepResult(step: { name: string; index?: number })`: Waits for a specific step to complete and return a result. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to target a specific occurrence. + * `waitForStatus(status: InstanceStatus["status"])`: Waits for the Workflow instance to reach a specific [status](/workflows/build/workers-api/#instancestatus) (e.g., 'running', 'complete'). + * `cleanUp()`: Cleans up the Workflow instance's state. It is crucial to call this after each test to ensure isolation. + +* `introspectWorkflow(workflow: Workflow)`: Promise\ + * Creates an **introspector** for a Workflow where instance IDs are unknown beforehand. This allows for defining modifications that will apply to **all subsequently created instances**. +
+ + ```ts + import { env, introspectWorkflow, SELF } from "cloudflare:test"; + + it("should disable all sleeps, mock an event and complete", async () => { + // 1. CONFIGURATION + const introspector = await introspectWorkflow(env.MY_WORKFLOW); + await introspector.modifyAll(async (m) => { + await m.disableSleeps(); + await m.mockEvent({ type: "user-approval" }, { approval: true }); + }); + + // 2. EXECUTION + await env.MY_WORKFLOW.create(); + + // 3. ASSERTION & CLEANUP + const instances = introspector.get(); + for(const instance of instances) { + await instance.waitForStatus("complete"); + await instance.cleanUp(); + } + + introspector.cleanUp(); + }); + ``` + The workflow instance doesn't have to be created inside the test. The workflow instance creation doesn't have to be direct. The introspector will capture **all** instances created after it is initialized. For example, you could trigger the creation of **one or multiple** instances via a single `fetch` event to your Worker: + ```js + // This also works for the EXECUTION phase: + await SELF.fetch("https://example.com/trigger-workflows"); + ``` + + * The returned `WorkflowIntrospector` object has the following methods: + * `modifyAll(fn: (m: WorkflowInstanceModifier) => Promise)`: Applies modifications to all Workflow instances created after calling `introspectWorkflow`. + * `get()`: Returns all `WorkflowInstanceIntrospector` objects from instances created after `introspectWorkflow` was called. + * `cleanUp()`: Cleans up and stops the introspection process for the Workflow. This is crucial to prevent modifications and captured instances from leaking between tests. + +* `WorkflowInstanceModifier` + * This object is provided to the `modify` and `modifyAll` callbacks to mock or alter the behavior of a Workflow instance's steps, events, and sleeps. + * `disableSleeps(steps?: { name: string; index?: number }[])`: Disables sleeps, causing `step.sleep()` and `step.sleepUntil()` to resolve immediately. If `steps` is omitted, all sleeps are disabled. + * `mockStepResult(step: { name: string; index?: number }, stepResult: unknown)`: Mocks the result of a `step.do()`, causing it to return the specified value instantly without executing the step's implementation. + * `mockStepError(step: { name: string; index?: number }, error: Error, times?: number)`: Forces a `step.do()` to throw an error, simulating a failure. Use the `times` parameter to test retry logic. + * `forceStepTimeout(step: { name: string; index?: number }, times?: number)`: Forces a `step.do()` to fail by timing out immediately. + * `mockEvent(event: { type: string; payload: unknown })`: Sends a mock event to the Workflow instance, causing a `step.waitForEvent()` to resolve with the provided payload. + * `forceEventTimeout(step: { name: string; index?: number })`: Forces a `step.waitForEvent()` to time out instantly, causing the step to fail. + + When targeting a step, use its `name`. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to specify the occurrence. \ No newline at end of file diff --git a/src/content/docs/workflows/build/test-workflows.mdx b/src/content/docs/workflows/build/test-workflows.mdx new file mode 100644 index 000000000000000..58063eb0c602ded --- /dev/null +++ b/src/content/docs/workflows/build/test-workflows.mdx @@ -0,0 +1,9 @@ +--- +title: Test Workflows +pcx_content_type: navigation +external_link: /workers/testing/vitest-integration/test-apis/#workflows +sidebar: + order: 11 + badge: + text: Beta +--- \ No newline at end of file From 123181e519a16324ee4f7d87332b388da7e77568 Mon Sep 17 00:00:00 2001 From: Olga Silva <78314353+pombosilva@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:38:59 +0100 Subject: [PATCH 2/8] Update src/content/docs/workers/testing/vitest-integration/test-apis.mdx Co-authored-by: Diogo Ferreira --- .../docs/workers/testing/vitest-integration/test-apis.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index 9b2fd9336a65dc9..710b870af36eae3 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -267,7 +267,7 @@ The Workers Vitest integration provides runtime helpers for writing tests in the :::caution[Workflows currently require `isolatedStorage` to be disabled] -To test projects that use Workflows, you **must** set `isolatedStorage` to `false` in your `vitest.config.ts` file. +To test projects that use Workflows `isolatedStorage` **must** be set to `false` in the project's `vitest.config.ts` file. ::: From a224645cdcae9b4c2f00e571e21eb7732fe5917a Mon Sep 17 00:00:00 2001 From: Olga Silva <78314353+pombosilva@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:39:22 +0100 Subject: [PATCH 3/8] Update src/content/docs/workers/testing/vitest-integration/test-apis.mdx Co-authored-by: Diogo Ferreira --- .../docs/workers/testing/vitest-integration/test-apis.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index 710b870af36eae3..81c1d224d20a019 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -261,7 +261,7 @@ The Workers Vitest integration provides runtime helpers for writing tests in the * Applies all un-applied [D1 migrations](/d1/reference/migrations/) stored in the `migrations` array to database `db`, recording migrations state in the `migrationsTableName` table. `migrationsTableName` defaults to `d1_migrations`. Call the [`readD1Migrations()`](/workers/testing/vitest-integration/configuration/#readd1migrationsmigrationspath) function from the `@cloudflare/vitest-pool-workers/config` package inside Node.js to get the `migrations` array. Refer to the [D1 recipe](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/d1) for an example project using migrations. -### Workflows +### Workflows From 5fd159d8eac5bb9dc7d2a30a3ba7cb9d89b4e8e3 Mon Sep 17 00:00:00 2001 From: Olga Silva <78314353+pombosilva@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:39:33 +0100 Subject: [PATCH 4/8] Update src/content/docs/workers/testing/vitest-integration/test-apis.mdx Co-authored-by: Diogo Ferreira --- .../docs/workers/testing/vitest-integration/test-apis.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index 81c1d224d20a019..f89027477cc36c2 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -8,7 +8,6 @@ description: Runtime helpers for writing tests, exported from the `cloudflare:te --- -import { InlineBadge } from '~/components'; The Workers Vitest integration provides runtime helpers for writing tests in the `cloudflare:test` module. The `cloudflare:test` module is provided by the `@cloudflare/vitest-pool-workers` package, but can only be imported from test files that execute in the Workers runtime. From bf920ca8875fdd51bb248598f647da5526d7921a Mon Sep 17 00:00:00 2001 From: Olga Silva Date: Mon, 1 Sep 2025 13:29:05 +0100 Subject: [PATCH 5/8] Add suggestions --- .../testing/vitest-integration/test-apis.mdx | 71 +++++++++++++++++-- .../docs/workflows/build/test-workflows.mdx | 2 - 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index f89027477cc36c2..96a7f2c253c0bc9 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -8,7 +8,6 @@ description: Runtime helpers for writing tests, exported from the `cloudflare:te --- - The Workers Vitest integration provides runtime helpers for writing tests in the `cloudflare:test` module. The `cloudflare:test` module is provided by the `@cloudflare/vitest-pool-workers` package, but can only be imported from test files that execute in the Workers runtime. ## `cloudflare:test` module definition @@ -282,7 +281,10 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` const instance = await introspectWorkflowInstance(env.MY_WORKFLOW, "123456"); await instance.modify(async (m) => { await m.disableSleeps(); - await m.mockEvent({ type: "user-approval" }, { approval: true }); + await m.mockEvent({ + type: "user-approval", + payload: { approved: true, approverId: "user-123" }, + }); }); // 2. EXECUTION @@ -299,7 +301,7 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` * `modify(fn: (m: WorkflowInstanceModifier) => Promise)`: Applies modifications to the Workflow instance's behavior. * `waitForStepResult(step: { name: string; index?: number })`: Waits for a specific step to complete and return a result. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to target a specific occurrence. * `waitForStatus(status: InstanceStatus["status"])`: Waits for the Workflow instance to reach a specific [status](/workflows/build/workers-api/#instancestatus) (e.g., 'running', 'complete'). - * `cleanUp()`: Cleans up the Workflow instance's state. It is crucial to call this after each test to ensure isolation. + * `cleanUp()`: Cleans up the Workflow instance's state, which is crucial for test isolation. If this function isn't called, the instance's state will persist across subsequent tests. For example, an instance that becomes completed in one test will already be completed at the start of the next. * `introspectWorkflow(workflow: Workflow)`: Promise\ * Creates an **introspector** for a Workflow where instance IDs are unknown beforehand. This allows for defining modifications that will apply to **all subsequently created instances**. @@ -313,7 +315,10 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` const introspector = await introspectWorkflow(env.MY_WORKFLOW); await introspector.modifyAll(async (m) => { await m.disableSleeps(); - await m.mockEvent({ type: "user-approval" }, { approval: true }); + await m.mockEvent({ + type: "user-approval", + payload: { approved: true, approverId: "user-123" }, + }); }); // 2. EXECUTION @@ -348,5 +353,61 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` * `forceStepTimeout(step: { name: string; index?: number }, times?: number)`: Forces a `step.do()` to fail by timing out immediately. * `mockEvent(event: { type: string; payload: unknown })`: Sends a mock event to the Workflow instance, causing a `step.waitForEvent()` to resolve with the provided payload. * `forceEventTimeout(step: { name: string; index?: number })`: Forces a `step.waitForEvent()` to time out instantly, causing the step to fail. + +
+ ```ts + import { env, introspectWorkflowInstance } from "cloudflare:test"; + + it("should apply all modifier functions", async () => { + // 1. CONFIGURATION + const instance = await introspectWorkflowInstance(env.COMPLEX_WORKFLOW, "123456"); + + // Modify instance behavior + await instance.modify(async (m) => { + // Disables all sleeps to make the test run instantly + await m.disableSleeps(); + + // Mocks the successful result of a data-fetching step + await m.mockStepResult( + { name: "get-order-details" }, + { orderId: "abc-123", amount: 99.99 } + ); + + // Mocks an incoming event to satisfy a `step.waitForEvent()` + await m.mockEvent({ + type: "user-approval", + payload: { approved: true, approverId: "user-123" }, + }); + + // Forces a step to fail once with a specific error to test retry logic + await m.mockStepError( + { name: "process-payment" }, + new Error("Payment gateway timeout"), + 1 // Fail only the first time + ); + + // Forces a `step.do()` to time out immediately + await m.forceStepTimeout({ name: "notify-shipping-partner" }); + + // Forces a `step.waitForEvent()` to time out + await m.forceEventTimeout({ name: "wait-for-fraud-check" }); + }); + + // 2. EXECUTION + await env.COMPLEX_WORKFLOW.create({ id: "123456" }); + + // 3. ASSERTION + expect(await instance.waitForStepResult({ name: "get-order-details" })).toEqual({ + orderId: "abc-123", + amount: 99.99, + }); + // Given the forced timeouts, the workflow will end in an errored state + await instance.waitForStatus("errored"); + + // 4. CLEANUP + await instance.cleanUp(); + }); + ``` - When targeting a step, use its `name`. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to specify the occurrence. \ No newline at end of file + When targeting a step, use its `name`. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to specify the occurrence. + \ No newline at end of file diff --git a/src/content/docs/workflows/build/test-workflows.mdx b/src/content/docs/workflows/build/test-workflows.mdx index 58063eb0c602ded..85546e0d34cac44 100644 --- a/src/content/docs/workflows/build/test-workflows.mdx +++ b/src/content/docs/workflows/build/test-workflows.mdx @@ -4,6 +4,4 @@ pcx_content_type: navigation external_link: /workers/testing/vitest-integration/test-apis/#workflows sidebar: order: 11 - badge: - text: Beta --- \ No newline at end of file From f7130b8ae3ce0ae80d76feac47b834b308a07377 Mon Sep 17 00:00:00 2001 From: Olga Silva Date: Tue, 9 Sep 2025 11:41:37 +0100 Subject: [PATCH 6/8] Add new testing Workflows cleanup logic --- .../testing/vitest-integration/recipes.mdx | 1 + .../testing/vitest-integration/test-apis.mdx | 39 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/content/docs/workers/testing/vitest-integration/recipes.mdx b/src/content/docs/workers/testing/vitest-integration/recipes.mdx index 1e2b60f5a807cf1..2288929a5611523 100644 --- a/src/content/docs/workers/testing/vitest-integration/recipes.mdx +++ b/src/content/docs/workers/testing/vitest-integration/recipes.mdx @@ -17,6 +17,7 @@ Recipes are examples that help demonstrate how to write unit tests and integrati - [Isolated tests using KV, R2 and the Cache API](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/kv-r2-caches) - [Isolated tests using D1 with migrations](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/d1) - [Isolated tests using Durable Objects with direct access](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/durable-objects) +- [Isolated tests using Workflows](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/workflows) - [Tests using Queue producers and consumers](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/queues) - [Tests using Hyperdrive with a Vitest managed TCP server](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/hyperdrive) - [Tests using declarative/imperative outbound request mocks](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/request-mocking) diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index 96a7f2c253c0bc9..6518999120de26d 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -263,9 +263,12 @@ The Workers Vitest integration provides runtime helpers for writing tests in the -:::caution[Workflows currently require `isolatedStorage` to be disabled] +:::caution[Workflows with `isolatedStorage`] -To test projects that use Workflows `isolatedStorage` **must** be set to `false` in the project's `vitest.config.ts` file. +To ensure proper test isolation in Workflows with isolated storage, introspectors should be cleaned up at the end of each test. +This is accomplished by either: +* **Automatic**: Using an `await using` statement on the introspector. +* **Manual**: Explicitly calling the introspector `cleanUp()` method. ::: @@ -278,7 +281,7 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` it("should disable all sleeps, mock an event and complete", async () => { // 1. CONFIGURATION - const instance = await introspectWorkflowInstance(env.MY_WORKFLOW, "123456"); + await using instance = await introspectWorkflowInstance(env.MY_WORKFLOW, "123456"); await instance.modify(async (m) => { await m.disableSleeps(); await m.mockEvent({ @@ -293,15 +296,15 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` // 3. ASSERTION await instance.waitForStatus("complete"); - // 4. CLEANUP - await instance.cleanUp(); + // 4. CLEANUP: automatic disposal }); ``` * The returned `WorkflowInstanceIntrospector` object has the following methods: - * `modify(fn: (m: WorkflowInstanceModifier) => Promise)`: Applies modifications to the Workflow instance's behavior. - * `waitForStepResult(step: { name: string; index?: number })`: Waits for a specific step to complete and return a result. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to target a specific occurrence. - * `waitForStatus(status: InstanceStatus["status"])`: Waits for the Workflow instance to reach a specific [status](/workflows/build/workers-api/#instancestatus) (e.g., 'running', 'complete'). - * `cleanUp()`: Cleans up the Workflow instance's state, which is crucial for test isolation. If this function isn't called, the instance's state will persist across subsequent tests. For example, an instance that becomes completed in one test will already be completed at the start of the next. + * `modify(fn: (m: WorkflowInstanceModifier) => Promise): Promise`: Applies modifications to the Workflow instance's behavior. + * `waitForStepResult(step: { name: string; index?: number }): Promise`: Waits for a specific step to complete and returns a result. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to target a specific occurrence. + * `waitForStatus(status: InstanceStatus["status"]): Promise`: Waits for the Workflow instance to reach a specific [status](/workflows/build/workers-api/#instancestatus) (e.g., 'running', 'complete'). + * `cleanUp(): Promise`: Cleans up the Workflow instance's state, which is crucial for test isolation. If this function isn't called and `await using` is not used, the instance's state will persist across subsequent tests. For example, an instance that becomes completed in one test will already be completed at the start of the next. + * `[Symbol.asyncDispose](): Provides automatic cleanup. It's invoked by the `await using` statement, which calls `cleanUp()`. * `introspectWorkflow(workflow: Workflow)`: Promise\ * Creates an **introspector** for a Workflow where instance IDs are unknown beforehand. This allows for defining modifications that will apply to **all subsequently created instances**. @@ -312,7 +315,7 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` it("should disable all sleeps, mock an event and complete", async () => { // 1. CONFIGURATION - const introspector = await introspectWorkflow(env.MY_WORKFLOW); + await using introspector = await introspectWorkflow(env.MY_WORKFLOW); await introspector.modifyAll(async (m) => { await m.disableSleeps(); await m.mockEvent({ @@ -324,17 +327,16 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` // 2. EXECUTION await env.MY_WORKFLOW.create(); - // 3. ASSERTION & CLEANUP + // 3. ASSERTION const instances = introspector.get(); for(const instance of instances) { await instance.waitForStatus("complete"); - await instance.cleanUp(); } - introspector.cleanUp(); + // 4. CLEANUP: automatic disposal }); ``` - The workflow instance doesn't have to be created inside the test. The workflow instance creation doesn't have to be direct. The introspector will capture **all** instances created after it is initialized. For example, you could trigger the creation of **one or multiple** instances via a single `fetch` event to your Worker: + The workflow instance doesn't have to be created directly inside the test. The introspector will capture **all** instances created after it is initialized. For example, you could trigger the creation of **one or multiple** instances via a single `fetch` event to your Worker: ```js // This also works for the EXECUTION phase: await SELF.fetch("https://example.com/trigger-workflows"); @@ -343,15 +345,16 @@ To test projects that use Workflows `isolatedStorage` **must** be set to `false` * The returned `WorkflowIntrospector` object has the following methods: * `modifyAll(fn: (m: WorkflowInstanceModifier) => Promise)`: Applies modifications to all Workflow instances created after calling `introspectWorkflow`. * `get()`: Returns all `WorkflowInstanceIntrospector` objects from instances created after `introspectWorkflow` was called. - * `cleanUp()`: Cleans up and stops the introspection process for the Workflow. This is crucial to prevent modifications and captured instances from leaking between tests. + * `cleanUp()`: Cleans up and stops the introspection process for the Workflow. All `WorkflowInstanceIntrospector` from created instances will also be cleaned. This is crucial to prevent modifications and captured instances from leaking between tests. After calling this method, the `WorkflowIntrospector` should not be reused. + * `[Symbol.asyncDispose](): Promise`: Provides automatic cleanup. It's invoked by the `await using` statement, which calls `cleanUp()`. * `WorkflowInstanceModifier` * This object is provided to the `modify` and `modifyAll` callbacks to mock or alter the behavior of a Workflow instance's steps, events, and sleeps. * `disableSleeps(steps?: { name: string; index?: number }[])`: Disables sleeps, causing `step.sleep()` and `step.sleepUntil()` to resolve immediately. If `steps` is omitted, all sleeps are disabled. * `mockStepResult(step: { name: string; index?: number }, stepResult: unknown)`: Mocks the result of a `step.do()`, causing it to return the specified value instantly without executing the step's implementation. - * `mockStepError(step: { name: string; index?: number }, error: Error, times?: number)`: Forces a `step.do()` to throw an error, simulating a failure. Use the `times` parameter to test retry logic. - * `forceStepTimeout(step: { name: string; index?: number }, times?: number)`: Forces a `step.do()` to fail by timing out immediately. - * `mockEvent(event: { type: string; payload: unknown })`: Sends a mock event to the Workflow instance, causing a `step.waitForEvent()` to resolve with the provided payload. + * `mockStepError(step: { name: string; index?: number }, error: Error, times?: number)`: Forces a `step.do()` to throw an error, simulating a failure. `times` is an optional number that sets how many times the step should error. If `times` is omitted, the step will error on every attempt, making the Workflow instance fail. + * `forceStepTimeout(step: { name: string; index?: number }, times?: number)`: Forces a `step.do()` to fail by timing out immediately. `times` is an optional number that sets how many times the step should timeout. If `times` is omitted, the step will timeout on every attempt, making the Workflow instance fail. + * `mockEvent(event: { type: string; payload: unknown })`: Sends a mock event to the Workflow instance, causing a `step.waitForEvent()` to resolve with the provided payload. `type` must match the `waitForEvent` type. * `forceEventTimeout(step: { name: string; index?: number })`: Forces a `step.waitForEvent()` to time out instantly, causing the step to fail.
From dec80a18242f0fe7540e4278367aa908b8c86f74 Mon Sep 17 00:00:00 2001 From: Olga Silva Date: Wed, 10 Sep 2025 11:47:56 +0100 Subject: [PATCH 7/8] CleanUp is now Dispose --- .../testing/vitest-integration/test-apis.mdx | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index 6518999120de26d..5a83aeb253d93a1 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -265,15 +265,15 @@ The Workers Vitest integration provides runtime helpers for writing tests in the :::caution[Workflows with `isolatedStorage`] -To ensure proper test isolation in Workflows with isolated storage, introspectors should be cleaned up at the end of each test. +To ensure proper test isolation in Workflows with isolated storage, introspectors should be disposed at the end of each test. This is accomplished by either: -* **Automatic**: Using an `await using` statement on the introspector. -* **Manual**: Explicitly calling the introspector `cleanUp()` method. +* Using an `await using` statement on the introspector. +* Explicitly calling the introspector `dispose()` method. ::: * `introspectWorkflowInstance(workflow: Workflow, instanceId: string)`: Promise\ - * Creates an **introspector** for a specific Workflow instance, used to **modify** its behavior, **await** outcomes, and **clean up** its state during tests. This is the primary entry point for testing individual Workflow instances with a known ID. + * Creates an **introspector** for a specific Workflow instance, used to **modify** its behavior, **await** outcomes, and **clear** its state during tests. This is the primary entry point for testing individual Workflow instances with a known ID.
```ts @@ -294,17 +294,17 @@ This is accomplished by either: await env.MY_WORKFLOW.create({ id: "123456" }); // 3. ASSERTION - await instance.waitForStatus("complete"); + await expect(instance.waitForStatus("complete")).resolves.not.toThrow(); - // 4. CLEANUP: automatic disposal + // 4. DISPOSE: is implicit and automatic here. }); ``` * The returned `WorkflowInstanceIntrospector` object has the following methods: * `modify(fn: (m: WorkflowInstanceModifier) => Promise): Promise`: Applies modifications to the Workflow instance's behavior. * `waitForStepResult(step: { name: string; index?: number }): Promise`: Waits for a specific step to complete and returns a result. If multiple steps share the same name, use the optional `index` property (1-based, defaults to `1`) to target a specific occurrence. * `waitForStatus(status: InstanceStatus["status"]): Promise`: Waits for the Workflow instance to reach a specific [status](/workflows/build/workers-api/#instancestatus) (e.g., 'running', 'complete'). - * `cleanUp(): Promise`: Cleans up the Workflow instance's state, which is crucial for test isolation. If this function isn't called and `await using` is not used, the instance's state will persist across subsequent tests. For example, an instance that becomes completed in one test will already be completed at the start of the next. - * `[Symbol.asyncDispose](): Provides automatic cleanup. It's invoked by the `await using` statement, which calls `cleanUp()`. + * `dispose(): Promise`: Disposes the Workflow instance, which is crucial for test isolation. If this function isn't called and `await using` is not used, isolated storage will fail and the instance's state will persist across subsequent tests. For example, an instance that becomes completed in one test will already be completed at the start of the next. + * `[Symbol.asyncDispose](): Provides automatic dispose. It's invoked by the `await using` statement, which calls `dispose()`. * `introspectWorkflow(workflow: Workflow)`: Promise\ * Creates an **introspector** for a Workflow where instance IDs are unknown beforehand. This allows for defining modifications that will apply to **all subsequently created instances**. @@ -330,10 +330,10 @@ This is accomplished by either: // 3. ASSERTION const instances = introspector.get(); for(const instance of instances) { - await instance.waitForStatus("complete"); + await expect(instance.waitForStatus("complete")).resolves.not.toThrow(); } - // 4. CLEANUP: automatic disposal + // 4. DISPOSE: is implicit and automatic here. }); ``` The workflow instance doesn't have to be created directly inside the test. The introspector will capture **all** instances created after it is initialized. For example, you could trigger the creation of **one or multiple** instances via a single `fetch` event to your Worker: @@ -343,10 +343,10 @@ This is accomplished by either: ``` * The returned `WorkflowIntrospector` object has the following methods: - * `modifyAll(fn: (m: WorkflowInstanceModifier) => Promise)`: Applies modifications to all Workflow instances created after calling `introspectWorkflow`. - * `get()`: Returns all `WorkflowInstanceIntrospector` objects from instances created after `introspectWorkflow` was called. - * `cleanUp()`: Cleans up and stops the introspection process for the Workflow. All `WorkflowInstanceIntrospector` from created instances will also be cleaned. This is crucial to prevent modifications and captured instances from leaking between tests. After calling this method, the `WorkflowIntrospector` should not be reused. - * `[Symbol.asyncDispose](): Promise`: Provides automatic cleanup. It's invoked by the `await using` statement, which calls `cleanUp()`. + * `modifyAll(fn: (m: WorkflowInstanceModifier) => Promise): Promise`: Applies modifications to all Workflow instances created after calling `introspectWorkflow`. + * `get(): Promise`: Returns all `WorkflowInstanceIntrospector` objects from instances created after `introspectWorkflow` was called. + * `dispose(): Promise`: Disposes the Workflow introspector. All `WorkflowInstanceIntrospector` from created instances will also be disposed. This is crucial to prevent modifications and captured instances from leaking between tests. After calling this method, the `WorkflowIntrospector` should not be reused. + * `[Symbol.asyncDispose](): Promise`: Provides automatic dispose. It's invoked by the `await using` statement, which calls `dispose()`. * `WorkflowInstanceModifier` * This object is provided to the `modify` and `modifyAll` callbacks to mock or alter the behavior of a Workflow instance's steps, events, and sleeps. @@ -361,54 +361,58 @@ This is accomplished by either: ```ts import { env, introspectWorkflowInstance } from "cloudflare:test"; + // This example showcases explicit disposal it("should apply all modifier functions", async () => { // 1. CONFIGURATION const instance = await introspectWorkflowInstance(env.COMPLEX_WORKFLOW, "123456"); - // Modify instance behavior - await instance.modify(async (m) => { - // Disables all sleeps to make the test run instantly - await m.disableSleeps(); - - // Mocks the successful result of a data-fetching step - await m.mockStepResult( - { name: "get-order-details" }, - { orderId: "abc-123", amount: 99.99 } - ); - - // Mocks an incoming event to satisfy a `step.waitForEvent()` - await m.mockEvent({ - type: "user-approval", - payload: { approved: true, approverId: "user-123" }, + try { + // Modify instance behavior + await instance.modify(async (m) => { + // Disables all sleeps to make the test run instantly + await m.disableSleeps(); + + // Mocks the successful result of a data-fetching step + await m.mockStepResult( + { name: "get-order-details" }, + { orderId: "abc-123", amount: 99.99 } + ); + + // Mocks an incoming event to satisfy a `step.waitForEvent()` + await m.mockEvent({ + type: "user-approval", + payload: { approved: true, approverId: "user-123" }, + }); + + // Forces a step to fail once with a specific error to test retry logic + await m.mockStepError( + { name: "process-payment" }, + new Error("Payment gateway timeout"), + 1 // Fail only the first time + ); + + // Forces a `step.do()` to time out immediately + await m.forceStepTimeout({ name: "notify-shipping-partner" }); + + // Forces a `step.waitForEvent()` to time out + await m.forceEventTimeout({ name: "wait-for-fraud-check" }); }); - // Forces a step to fail once with a specific error to test retry logic - await m.mockStepError( - { name: "process-payment" }, - new Error("Payment gateway timeout"), - 1 // Fail only the first time - ); - - // Forces a `step.do()` to time out immediately - await m.forceStepTimeout({ name: "notify-shipping-partner" }); + // 2. EXECUTION + await env.COMPLEX_WORKFLOW.create({ id: "123456" }); - // Forces a `step.waitForEvent()` to time out - await m.forceEventTimeout({ name: "wait-for-fraud-check" }); - }); - - // 2. EXECUTION - await env.COMPLEX_WORKFLOW.create({ id: "123456" }); - - // 3. ASSERTION - expect(await instance.waitForStepResult({ name: "get-order-details" })).toEqual({ - orderId: "abc-123", - amount: 99.99, - }); - // Given the forced timeouts, the workflow will end in an errored state - await instance.waitForStatus("errored"); + // 3. ASSERTION + expect(await instance.waitForStepResult({ name: "get-order-details" })).toEqual({ + orderId: "abc-123", + amount: 99.99, + }); + // Given the forced timeouts, the workflow will end in an errored state + await expect(instance.waitForStatus("errored")).resolves.not.toThrow(); - // 4. CLEANUP - await instance.cleanUp(); + } catch { + // 4. DISPOSE + await instance.dispose(); + } }); ``` From a4fb0b7b4fc8ae352467609ad94367be7e328ba9 Mon Sep 17 00:00:00 2001 From: Olga Silva Date: Fri, 12 Sep 2025 13:02:41 +0100 Subject: [PATCH 8/8] Add version and changelog --- .../docs/workers/testing/vitest-integration/test-apis.mdx | 6 ++++++ src/content/release-notes/workflows.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx index 5a83aeb253d93a1..ca51ccfe92b7328 100644 --- a/src/content/docs/workers/testing/vitest-integration/test-apis.mdx +++ b/src/content/docs/workers/testing/vitest-integration/test-apis.mdx @@ -272,6 +272,12 @@ This is accomplished by either: ::: +:::note[Version] + +Available in `@cloudflare/vitest-pool-workers` version **0.9.0**! + +::: + * `introspectWorkflowInstance(workflow: Workflow, instanceId: string)`: Promise\ * Creates an **introspector** for a specific Workflow instance, used to **modify** its behavior, **await** outcomes, and **clear** its state during tests. This is the primary entry point for testing individual Workflow instances with a known ID.
diff --git a/src/content/release-notes/workflows.yaml b/src/content/release-notes/workflows.yaml index ef4f05398314ef2..a8ab47c5b3ca970 100644 --- a/src/content/release-notes/workflows.yaml +++ b/src/content/release-notes/workflows.yaml @@ -3,6 +3,12 @@ link: "/workflows/reference/changelog/" productName: Workflows productLink: "/workflows/" entries: + - publish_date: "2025-09-12" + title: "Test Workflows locally" + description: |- + Workflows can now be tested with new test APIs available in the "cloudflare:test" module. + + More information available in the Vitest integration [docs](/workers/testing/vitest-integration/test-apis/#workflows). - publish_date: "2025-05-07" title: "Search for specific Workflows" description: |-