diff --git a/www/apps/book/app/learn/debugging-and-testing/debug-workflows/page.mdx b/www/apps/book/app/learn/debugging-and-testing/debug-workflows/page.mdx
index 8b87ee82a5..dc548279cd 100644
--- a/www/apps/book/app/learn/debugging-and-testing/debug-workflows/page.mdx
+++ b/www/apps/book/app/learn/debugging-and-testing/debug-workflows/page.mdx
@@ -28,7 +28,7 @@ There are several ways to debug workflows in Medusa:
- Write integration tests
+ [Write integration tests](#approach-1-write-integration-tests)
To ensure your workflow produces the expected results and handles edge cases.
@@ -36,7 +36,7 @@ There are several ways to debug workflows in Medusa:
- Add breakpoints
+ [Add breakpoints](#approach-2-add-breakpoints)
To inspect specific steps in your workflow and understand the data flow.
@@ -44,7 +44,7 @@ There are several ways to debug workflows in Medusa:
- Log messages
+ [Log messages](#approach-3-log-messages)
To check values during execution with minimal overhead.
@@ -52,7 +52,7 @@ There are several ways to debug workflows in Medusa:
- View Workflow Executions in Medusa Admin
+ [View Workflow Executions in Medusa Admin](#approach-4-monitor-workflow-executions-in-medusa-admin)
To monitor stored workflow executions and long-running workflows, especially in production environments.
@@ -140,12 +140,12 @@ import {
createWorkflow,
StepResponse,
WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk";
+} from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async () => {
- const message = "Hello from step 1!";
+ const message = "Hello from step 1!"
return new StepResponse(
message
@@ -171,10 +171,10 @@ export const myWorkflow = createWorkflow(
step2()
return new WorkflowResponse({
- response
+ response,
})
}
-);
+)
```
@@ -424,12 +424,12 @@ import {
createWorkflow,
StepResponse,
WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk";
+} from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async () => {
- const message = "Hello from step 1!";
+ const message = "Hello from step 1!"
return new StepResponse(
message
@@ -447,10 +447,10 @@ export const myWorkflow = createWorkflow(
const response = step1()
return new WorkflowResponse({
- response
+ response,
})
}
-);
+)
```
@@ -460,12 +460,3 @@ Refer to the [Store Workflow Executions](../../fundamentals/workflows/store-exec
You can view all executions of this workflow in the Medusa Admin under the [Workflows settings page](!user-guide!/settings/developer/workflows). Each execution will show you the status, input, and output data.
-
----
-
-## Related Topics
-
-- [Error Handling in Workflows](../../fundamentals/workflows/errors/page.mdx) - For debugging error scenarios and compensation functions
-- [Long-Running Workflows](../../fundamentals/workflows/long-running-workflow/page.mdx) - For debugging async workflows and step status management
-- [Store Workflow Executions](../../fundamentals/workflows/store-executions/page.mdx) - For programmatic access to execution details
-- [Workflow Execution States](!user-guide!/settings/developer/workflows#workflow-execution-status) - For understanding execution statuses in the admin
diff --git a/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx b/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx
index 92e543cef4..66727b39de 100644
--- a/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx
+++ b/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx
@@ -6,7 +6,13 @@ export const metadata = {
# {metadata.title}
-In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](../page.mdx) from Medusa's Testing Framwork.
+In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](../page.mdx) from Medusa's Testing Framework.
+
+
+
+For other debugging approaches, refer to the [Debug Workflows](../../../debug-workflows/page.mdx) chapter.
+
+
-## Write Integration Test for Workflow
+## Write Integration Test for a Workflow
Consider you have the following workflow defined at `src/workflows/hello-world.ts`:
@@ -65,7 +71,7 @@ medusaIntegrationTestRunner({
jest.setTimeout(60 * 1000)
```
-You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`.
+You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test passes if the workflow returns the string `"Hello, World!"`.
### Jest Timeout
@@ -78,32 +84,32 @@ jest.setTimeout(60 * 1000)
---
-## Run Test
+## Run Tests
Run the following command to run your tests:
```bash npm2yarn
-npm run test:integration
+npm run test:integration:http
```
-If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](../../page.mdx#add-test-commands).
+If you don't have a `test:integration:http` script in `package.json`, refer to the [Medusa Testing Tools chapter](../../page.mdx#add-test-commands).
-This runs your Medusa application and runs the tests available under the `integrations/http` directory.
+This runs your Medusa application and runs the tests available under the `integration-tests/http` directory.
---
## Test That a Workflow Throws an Error
-You might want to test that a workflow throws an error in certain cases. To test this:
+You might want to verify that a workflow throws an error in certain edge cases. To test that a workflow throws an error:
- Disable the `throwOnError` option when executing the workflow.
- Use the returned `errors` property to check what errors were thrown.
-For example, if you have a step that throws this error:
+For example, if you have the following step in your workflow that throws a `MedusaError`:
```ts title="src/workflows/hello-world.ts"
import { MedusaError } from "@medusajs/framework/utils"
@@ -123,7 +129,7 @@ import { helloWorldWorkflow } from "../../src/workflows/hello-world"
medusaIntegrationTestRunner({
testSuite: ({ getContainer }) => {
describe("Test hello-world workflow", () => {
- it("returns message", async () => {
+ it("should throw error when item doesn't exist", async () => {
const { errors } = await helloWorldWorkflow(getContainer())
.run({
throwOnError: false,
@@ -139,6 +145,432 @@ medusaIntegrationTestRunner({
jest.setTimeout(60 * 1000)
```
-The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown.
+The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, which is the error thrown.
-If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`.
\ No newline at end of file
+If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`.
+
+---
+
+## Test Long-Running Workflows
+
+Since [long-running workflows](../../../../fundamentals/workflows/long-running-workflow/page.mdx) run asynchronously, testing them requires a different approach than synchronous workflows.
+
+When testing long-running workflows, you need to:
+
+1. Set the asynchronous steps as successful manually.
+2. Subscribe to the workflow's events to listen for the workflow execution's completion.
+3. Verify the output of the workflow after it has completed.
+
+For example, consider you have the following long-running workflow defined at `src/workflows/long-running-workflow.ts`:
+
+```ts title="src/workflows/long-running-workflow.ts" highlights={[["15"]]}
+import {
+ createStep,
+ createWorkflow,
+ WorkflowResponse,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+
+const step1 = createStep("step-1", async () => {
+ return new StepResponse({})
+})
+
+const step2 = createStep(
+ {
+ name: "step-2",
+ async: true,
+ },
+ async () => {
+ console.log("Waiting to be successful...")
+ }
+)
+
+const step3 = createStep("step-3", async () => {
+ return new StepResponse("Finished three steps")
+})
+
+const longRunningWorkflow = createWorkflow(
+ "long-running",
+ function () {
+ step1()
+ step2()
+ const message = step3()
+
+ return new WorkflowResponse({
+ message,
+ })
+ }
+)
+
+export default longRunningWorkflow
+```
+
+`step2` in this workflow is an asynchronous step that you need to set as successful manually in your test.
+
+You can write the following test to ensure that the long-running workflow completes successfully:
+
+export const longRunningWorkflowHighlights1 = [
+ ["11", "longRunningWorkflow", "Execute the long-running workflow."],
+ ["14", "workflowEngineService", "Resolve the Workflow Engine Module's service."],
+ ["19", "workflowCompletion", "Create a promise to wait for the workflow's completion."],
+ ["30", "subscribe", "Subscribe to the workflow's events."],
+ ["44", "setStepSuccess", "Set the asynchronous step as successful."],
+ ["54", "afterSubscriber", "Wait for the promise to resolve when workflow execution completes."],
+ ["56", "expect", "Assert that the workflow's result matches the expected output."],
+]
+
+```ts title="integration-tests/http/long-running-workflow.spec.ts" highlights={longRunningWorkflowHighlights1}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import longRunningWorkflow from "../../src/workflows/long-running-workflow"
+import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
+import { StepResponse } from "@medusajs/framework/workflows-sdk"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ describe("Test long-running workflow", () => {
+ it("returns message", async () => {
+ const container = getContainer()
+ const { transaction } = await longRunningWorkflow(container)
+ .run()
+
+ const workflowEngineService = container.resolve(
+ Modules.WORKFLOW_ENGINE
+ )
+
+ let workflowOk: any
+ const workflowCompletion = new Promise((ok) => {
+ workflowOk = ok
+ })
+
+ const subscriptionOptions = {
+ workflowId: "long-running",
+ transactionId: transaction.transactionId,
+ subscriberId: "long-running-subscriber",
+ }
+
+
+ await workflowEngineService.subscribe({
+ ...subscriptionOptions,
+ subscriber: async (data) => {
+ if (data.eventType === "onFinish") {
+ workflowOk(data.result.message)
+ // unsubscribe
+ await workflowEngineService.unsubscribe({
+ ...subscriptionOptions,
+ subscriberOrId: subscriptionOptions.subscriberId,
+ })
+ }
+ },
+ })
+
+ await workflowEngineService.setStepSuccess({
+ idempotencyKey: {
+ action: TransactionHandlerType.INVOKE,
+ transactionId: transaction.transactionId,
+ stepId: "step-2",
+ workflowId: "long-running",
+ },
+ stepResponse: new StepResponse("Done!"),
+ })
+
+ const afterSubscriber = await workflowCompletion
+
+ expect(afterSubscriber).toBe("Finished three steps")
+ })
+ })
+ },
+})
+
+jest.setTimeout(60 * 1000)
+```
+
+In this test, you:
+
+1. Execute the long-running workflow and get the transaction details from the `run` method's result.
+2. Resolve the [Workflow Engine Module](!resources!/infrastructure-modules/workflow-engine)'s service from the Medusa container.
+3. Create a promise to wait for the workflow's completion.
+4. Subscribe to the workflow's events using the Workflow Engine Module's `subscribe` method.
+ - The `subscriber` function is called whenever an event related to the workflow occurs. On the `onFinish` event that indicates the workflow has completed, you resolve the promise with the workflow's result.
+5. Set the asynchronous step as successful using the `setStepSuccess` method of the Workflow Engine Module.
+6. Wait for the promise to resolve, which indicates that the workflow has completed successfully.
+7. Finally, you assert that the workflow's result matches the expected output.
+
+If you run the integration test, it will execute the long-running workflow and verify that it completes and returns the expected result.
+
+### Example with Multiple Asynchronous Steps
+
+If your long-running workflow has multiple asynchronous steps, you must set each of them as successful in your test before the workflow can complete.
+
+Here's how the test would look like if you had two asynchronous steps:
+
+export const longRunningWorkflowHighlights2 = [
+ ["43", "setStepSuccess", "Set the first asynchronous step as successful."],
+ ["53", "setStepSuccess", "Set the second asynchronous step as successful."],
+]
+
+```ts title="integration-tests/http/long-running-workflow-multiple-steps.spec.ts" highlights={longRunningWorkflowHighlights2}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import longRunningWorkflow from "../../src/workflows/long-running-workflow"
+import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
+import { StepResponse } from "@medusajs/framework/workflows-sdk"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ describe("Test long-running workflow with multiple async steps", () => {
+ it("returns message", async () => {
+ const container = getContainer()
+ const { transaction } = await longRunningWorkflow(container)
+ .run()
+
+ const workflowEngineService = container.resolve(
+ Modules.WORKFLOW_ENGINE
+ )
+
+ let workflowOk: any
+ const workflowCompletion = new Promise((ok) => {
+ workflowOk = ok
+ })
+
+ const subscriptionOptions = {
+ workflowId: "long-running",
+ transactionId: transaction.transactionId,
+ subscriberId: "long-running-subscriber",
+ }
+
+ await workflowEngineService.subscribe({
+ ...subscriptionOptions,
+ subscriber: async (data) => {
+ if (data.eventType === "onFinish") {
+ workflowOk(data.result.message)
+ // unsubscribe
+ await workflowEngineService.unsubscribe({
+ ...subscriptionOptions,
+ subscriberOrId: subscriptionOptions.subscriberId,
+ })
+ }
+ },
+ })
+
+ await workflowEngineService.setStepSuccess({
+ idempotencyKey: {
+ action: TransactionHandlerType.INVOKE,
+ transactionId: transaction.transactionId,
+ stepId: "step-2",
+ workflowId: "long-running",
+ },
+ stepResponse: new StepResponse("Done!"),
+ })
+
+ await workflowEngineService.setStepSuccess({
+ idempotencyKey: {
+ action: TransactionHandlerType.INVOKE,
+ transactionId: transaction.transactionId,
+ stepId: "step-3",
+ workflowId: "long-running",
+ },
+ stepResponse: new StepResponse("Done with step 3!"),
+ })
+
+ const afterSubscriber = await workflowCompletion
+
+ expect(afterSubscriber).toBe("Finished three steps")
+ })
+ })
+ },
+})
+```
+
+In this example, you set both `step-2` and `step-3` as successful before waiting for the workflow to complete.
+
+---
+
+## Test Database Operations in Workflows
+
+In real use cases, you'll often test workflows that perform database operations, such as creating a brand.
+
+When you test such workflows, you may need to:
+
+- Verify that the database operations were performed correctly. For example, that a brand was created with the expected properties.
+- Perform database actions before testing the workflow. For example, creating a brand before testing a workflow that deletes it.
+
+This section provides examples of both scenarios.
+
+### Verify Database Operations in Workflow Test
+
+To retrieve data from the database after running a workflow, you can resolve and use either the module's service (for example, the Brand Module's service) or [Query](../../../../fundamentals/module-links/query/page.mdx).
+
+For example, the following test verifies that a brand was created by a workflow:
+
+export const workflowBrandHighlights = [
+ ["10", "createBrandWorkflow", "Execute the create brand workflow."],
+ ["17", "brandModuleService", "Resolve the Brand Module's service."],
+ ["19", "retrieveBrand", "Retrieve the created brand from the database."],
+ ["21", "expect", "Assert that the brand was created with the expected properties."],
+]
+
+```ts title="integration-tests/http/workflow-brand.spec.ts" highlights={workflowBrandHighlights}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import { createBrandWorkflow } from "../../src/workflows/create-brand"
+import { BRAND_MODULE } from "../../src/modules/brand"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ describe("Test create brand workflow", () => {
+ it("creates a brand", async () => {
+ const container = getContainer()
+ const { result: brand } = await createBrandWorkflow(container)
+ .run({
+ input: {
+ name: "Test Brand",
+ },
+ })
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+
+ const createdBrand = await brandModuleService.retrieveBrand(brand.id)
+ expect(createdBrand).toBeDefined()
+ expect(createdBrand.name).toBe("Test Brand")
+ })
+ })
+ },
+})
+
+jest.setTimeout(60 * 1000)
+```
+
+In this test, you run the workflow, which creates a brand. Then, you retrieve the brand from the database using the Brand Module's service and verify that it was created with the expected properties.
+
+### Perform Database Actions Before Testing Workflow
+
+You can perform database actions before testing workflows in the `beforeAll` or `beforeEach` hooks of your test suite. In those hooks, you can create data that is useful for your workflow tests.
+
+
+
+Learn more about test hooks in [Jest's Documentation](https://jestjs.io/docs/setup-teardown).
+
+
+
+You can perform the database actions before testing a workflow by either:
+
+- Using the module's service (for example, the Brand Module's service).
+- Using an existing workflow that performs the database actions.
+
+#### Use Module's Service
+
+For example, the following test creates a brand using the Brand Module's service before running the workflow that deletes it:
+
+export const workflowBrandDeleteHighlights = [
+ ["14", "createBrands", "Create a brand using the Brand Module's service."],
+ ["24", "deleteBrandWorkflow", "Run the delete brand workflow."],
+ ["34", "expect", "Assert that the brand was deleted successfully."],
+]
+
+```ts title="integration-tests/http/workflow-brand-delete.spec.ts"
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"
+import { BRAND_MODULE } from "../../src/modules/brand"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ let brandId: string
+
+ beforeAll(async () => {
+ const container = getContainer()
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+
+ const brand = await brandModuleService.createBrands({
+ name: "Test Brand",
+ })
+
+ brandId = brand.id
+ })
+
+ describe("Test delete brand workflow", () => {
+ it("deletes a brand", async () => {
+ const container = getContainer()
+ const { result } = await deleteBrandWorkflow(container)
+ .run({
+ input: {
+ id: brandId,
+ },
+ })
+
+ expect(result.success).toBe(true)
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+ await expect(brandModuleService.retrieveBrand(brandId))
+ .rejects.toThrow()
+ })
+ })
+ },
+})
+```
+
+In this example, you:
+
+1. Use the `beforeAll` hook to create a brand before running the workflow that deletes it.
+2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand.
+3. Verify that the brand was deleted successfully by checking that retrieving it throws an error.
+
+#### Use Existing Workflow
+
+Alternatively, if you already have a workflow that performs the database operations, you can use that workflow in the `beforeAll` or `beforeEach` hook. This is useful if the database operations are complex and are already encapsulated in a workflow.
+
+For example, you can modify the `beforeAll` hook to use the `createBrandWorkflow`:
+
+export const workflowBrandDeleteHighlights2 = [
+ ["13", "createBrandWorkflow", "Create a brand using the create brand workflow."],
+ ["26", "deleteBrandWorkflow", "Run the delete brand workflow."],
+ ["36", "expect", "Assert that the brand was deleted successfully."],
+]
+
+```ts title="integration-tests/http/workflow-brand-delete.spec.ts" highlights={workflowBrandDeleteHighlights2}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"
+import { createBrandWorkflow } from "../../src/workflows/create-brand"
+import { BRAND_MODULE } from "../../src/modules/brand"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ let brandId: string
+
+ beforeAll(async () => {
+ const container = getContainer()
+
+ const { result: brand } = await createBrandWorkflow(container)
+ .run({
+ input: {
+ name: "Test Brand",
+ },
+ })
+
+ brandId = brand.id
+ })
+
+ describe("Test delete brand workflow", () => {
+ it("deletes a brand", async () => {
+ const container = getContainer()
+ const { result } = await deleteBrandWorkflow(container)
+ .run({
+ input: {
+ id: brandId,
+ },
+ })
+
+ expect(result.success).toBe(true)
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+ await expect(brandModuleService.retrieveBrand(brandId))
+ .rejects.toThrow()
+ })
+ })
+ },
+})
+```
+
+In this example, you:
+
+1. Use the `beforeAll` hook to run the `createBrandWorkflow`, which creates a brand before running the workflow that deletes it.
+2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand.
+3. Verify that the brand was deleted successfully by checking that retrieving it throws an error.
diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs
index f3409924ee..e1b2d470e2 100644
--- a/www/apps/book/generated/edit-dates.mjs
+++ b/www/apps/book/generated/edit-dates.mjs
@@ -52,7 +52,7 @@ export const generatedEditDates = {
"app/learn/fundamentals/custom-cli-scripts/page.mdx": "2025-07-25T15:32:47.587Z",
"app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx": "2025-03-18T15:06:27.864Z",
"app/learn/debugging-and-testing/testing-tools/integration-tests/page.mdx": "2024-12-09T15:52:01.019Z",
- "app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2025-02-11T15:56:03.835Z",
+ "app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2025-07-30T13:43:44.636Z",
"app/learn/debugging-and-testing/testing-tools/page.mdx": "2025-07-23T15:32:18.008Z",
"app/learn/debugging-and-testing/testing-tools/unit-tests/module-example/page.mdx": "2024-09-02T11:04:27.232Z",
"app/learn/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z",
@@ -126,5 +126,5 @@ export const generatedEditDates = {
"app/learn/installation/docker/page.mdx": "2025-07-23T15:34:18.530Z",
"app/learn/fundamentals/generated-types/page.mdx": "2025-07-25T13:17:35.319Z",
"app/learn/introduction/from-v1-to-v2/page.mdx": "2025-07-30T08:13:48.592Z",
- "app/learn/debugging-and-testing/debug-workflows/page.mdx": "2025-07-30T10:38:41.398Z"
+ "app/learn/debugging-and-testing/debug-workflows/page.mdx": "2025-07-30T13:45:14.117Z"
}
\ No newline at end of file
diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt
index d9f1433bc5..9e41bcd17a 100644
--- a/www/apps/book/public/llms-full.txt
+++ b/www/apps/book/public/llms-full.txt
@@ -3575,12 +3575,12 @@ import {
createWorkflow,
StepResponse,
WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk";
+} from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async () => {
- const message = "Hello from step 1!";
+ const message = "Hello from step 1!"
return new StepResponse(
message
@@ -3606,10 +3606,10 @@ export const myWorkflow = createWorkflow(
step2()
return new WorkflowResponse({
- response
+ response,
})
}
-);
+)
```
### Transform Callback
@@ -3842,12 +3842,12 @@ import {
createWorkflow,
StepResponse,
WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk";
+} from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async () => {
- const message = "Hello from step 1!";
+ const message = "Hello from step 1!"
return new StepResponse(
message
@@ -3865,25 +3865,16 @@ export const myWorkflow = createWorkflow(
const response = step1()
return new WorkflowResponse({
- response
+ response,
})
}
-);
+)
```
Refer to the [Store Workflow Executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md) chapter to learn more.
You can view all executions of this workflow in the Medusa Admin under the [Workflows settings page](https://docs.medusajs.com/user-guide/settings/developer/workflows/index.html.md). Each execution will show you the status, input, and output data.
-***
-
-## Related Topics
-
-- [Error Handling in Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md) - For debugging error scenarios and compensation functions
-- [Long-Running Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) - For debugging async workflows and step status management
-- [Store Workflow Executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md) - For programmatic access to execution details
-- [Workflow Execution States](https://docs.medusajs.com/user-guide/settings/developer/workflows#workflow-execution-status/index.html.md) - For understanding execution statuses in the admin
-
# Configure Instrumentation
@@ -4804,13 +4795,15 @@ The next chapters provide examples of writing integration tests for API routes a
# Example: Write Integration Tests for Workflows
-In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork.
+In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework.
+
+For other debugging approaches, refer to the [Debug Workflows](https://docs.medusajs.com/learn/debugging-and-testing/debug-workflows/index.html.md) chapter.
### Prerequisites
- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
-## Write Integration Test for Workflow
+## Write Integration Test for a Workflow
Consider you have the following workflow defined at `src/workflows/hello-world.ts`:
@@ -4858,7 +4851,7 @@ medusaIntegrationTestRunner({
jest.setTimeout(60 * 1000)
```
-You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`.
+You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test passes if the workflow returns the string `"Hello, World!"`.
### Jest Timeout
@@ -4871,28 +4864,28 @@ jest.setTimeout(60 * 1000)
***
-## Run Test
+## Run Tests
Run the following command to run your tests:
```bash npm2yarn
-npm run test:integration
+npm run test:integration:http
```
-If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).
+If you don't have a `test:integration:http` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).
-This runs your Medusa application and runs the tests available under the `integrations/http` directory.
+This runs your Medusa application and runs the tests available under the `integration-tests/http` directory.
***
## Test That a Workflow Throws an Error
-You might want to test that a workflow throws an error in certain cases. To test this:
+You might want to verify that a workflow throws an error in certain edge cases. To test that a workflow throws an error:
- Disable the `throwOnError` option when executing the workflow.
- Use the returned `errors` property to check what errors were thrown.
-For example, if you have a step that throws this error:
+For example, if you have the following step in your workflow that throws a `MedusaError`:
```ts title="src/workflows/hello-world.ts"
import { MedusaError } from "@medusajs/framework/utils"
@@ -4912,7 +4905,7 @@ import { helloWorldWorkflow } from "../../src/workflows/hello-world"
medusaIntegrationTestRunner({
testSuite: ({ getContainer }) => {
describe("Test hello-world workflow", () => {
- it("returns message", async () => {
+ it("should throw error when item doesn't exist", async () => {
const { errors } = await helloWorldWorkflow(getContainer())
.run({
throwOnError: false,
@@ -4928,10 +4921,398 @@ medusaIntegrationTestRunner({
jest.setTimeout(60 * 1000)
```
-The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown.
+The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, which is the error thrown.
If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`.
+***
+
+## Test Long-Running Workflows
+
+Since [long-running workflows](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) run asynchronously, testing them requires a different approach than synchronous workflows.
+
+When testing long-running workflows, you need to:
+
+1. Set the asynchronous steps as successful manually.
+2. Subscribe to the workflow's events to listen for the workflow execution's completion.
+3. Verify the output of the workflow after it has completed.
+
+For example, consider you have the following long-running workflow defined at `src/workflows/long-running-workflow.ts`:
+
+```ts title="src/workflows/long-running-workflow.ts" highlights={[["15"]]}
+import {
+ createStep,
+ createWorkflow,
+ WorkflowResponse,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+
+const step1 = createStep("step-1", async () => {
+ return new StepResponse({})
+})
+
+const step2 = createStep(
+ {
+ name: "step-2",
+ async: true,
+ },
+ async () => {
+ console.log("Waiting to be successful...")
+ }
+)
+
+const step3 = createStep("step-3", async () => {
+ return new StepResponse("Finished three steps")
+})
+
+const longRunningWorkflow = createWorkflow(
+ "long-running",
+ function () {
+ step1()
+ step2()
+ const message = step3()
+
+ return new WorkflowResponse({
+ message,
+ })
+ }
+)
+
+export default longRunningWorkflow
+```
+
+`step2` in this workflow is an asynchronous step that you need to set as successful manually in your test.
+
+You can write the following test to ensure that the long-running workflow completes successfully:
+
+```ts title="integration-tests/http/long-running-workflow.spec.ts" highlights={longRunningWorkflowHighlights1}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import longRunningWorkflow from "../../src/workflows/long-running-workflow"
+import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
+import { StepResponse } from "@medusajs/framework/workflows-sdk"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ describe("Test long-running workflow", () => {
+ it("returns message", async () => {
+ const container = getContainer()
+ const { transaction } = await longRunningWorkflow(container)
+ .run()
+
+ const workflowEngineService = container.resolve(
+ Modules.WORKFLOW_ENGINE
+ )
+
+ let workflowOk: any
+ const workflowCompletion = new Promise((ok) => {
+ workflowOk = ok
+ })
+
+ const subscriptionOptions = {
+ workflowId: "long-running",
+ transactionId: transaction.transactionId,
+ subscriberId: "long-running-subscriber",
+ }
+
+
+ await workflowEngineService.subscribe({
+ ...subscriptionOptions,
+ subscriber: async (data) => {
+ if (data.eventType === "onFinish") {
+ workflowOk(data.result.message)
+ // unsubscribe
+ await workflowEngineService.unsubscribe({
+ ...subscriptionOptions,
+ subscriberOrId: subscriptionOptions.subscriberId,
+ })
+ }
+ },
+ })
+
+ await workflowEngineService.setStepSuccess({
+ idempotencyKey: {
+ action: TransactionHandlerType.INVOKE,
+ transactionId: transaction.transactionId,
+ stepId: "step-2",
+ workflowId: "long-running",
+ },
+ stepResponse: new StepResponse("Done!"),
+ })
+
+ const afterSubscriber = await workflowCompletion
+
+ expect(afterSubscriber).toBe("Finished three steps")
+ })
+ })
+ },
+})
+
+jest.setTimeout(60 * 1000)
+```
+
+In this test, you:
+
+1. Execute the long-running workflow and get the transaction details from the `run` method's result.
+2. Resolve the [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md)'s service from the Medusa container.
+3. Create a promise to wait for the workflow's completion.
+4. Subscribe to the workflow's events using the Workflow Engine Module's `subscribe` method.
+ - The `subscriber` function is called whenever an event related to the workflow occurs. On the `onFinish` event that indicates the workflow has completed, you resolve the promise with the workflow's result.
+5. Set the asynchronous step as successful using the `setStepSuccess` method of the Workflow Engine Module.
+6. Wait for the promise to resolve, which indicates that the workflow has completed successfully.
+7. Finally, you assert that the workflow's result matches the expected output.
+
+If you run the integration test, it will execute the long-running workflow and verify that it completes and returns the expected result.
+
+### Example with Multiple Asynchronous Steps
+
+If your long-running workflow has multiple asynchronous steps, you must set each of them as successful in your test before the workflow can complete.
+
+Here's how the test would look like if you had two asynchronous steps:
+
+```ts title="integration-tests/http/long-running-workflow-multiple-steps.spec.ts" highlights={longRunningWorkflowHighlights2}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import longRunningWorkflow from "../../src/workflows/long-running-workflow"
+import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
+import { StepResponse } from "@medusajs/framework/workflows-sdk"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ describe("Test long-running workflow with multiple async steps", () => {
+ it("returns message", async () => {
+ const container = getContainer()
+ const { transaction } = await longRunningWorkflow(container)
+ .run()
+
+ const workflowEngineService = container.resolve(
+ Modules.WORKFLOW_ENGINE
+ )
+
+ let workflowOk: any
+ const workflowCompletion = new Promise((ok) => {
+ workflowOk = ok
+ })
+
+ const subscriptionOptions = {
+ workflowId: "long-running",
+ transactionId: transaction.transactionId,
+ subscriberId: "long-running-subscriber",
+ }
+
+ await workflowEngineService.subscribe({
+ ...subscriptionOptions,
+ subscriber: async (data) => {
+ if (data.eventType === "onFinish") {
+ workflowOk(data.result.message)
+ // unsubscribe
+ await workflowEngineService.unsubscribe({
+ ...subscriptionOptions,
+ subscriberOrId: subscriptionOptions.subscriberId,
+ })
+ }
+ },
+ })
+
+ await workflowEngineService.setStepSuccess({
+ idempotencyKey: {
+ action: TransactionHandlerType.INVOKE,
+ transactionId: transaction.transactionId,
+ stepId: "step-2",
+ workflowId: "long-running",
+ },
+ stepResponse: new StepResponse("Done!"),
+ })
+
+ await workflowEngineService.setStepSuccess({
+ idempotencyKey: {
+ action: TransactionHandlerType.INVOKE,
+ transactionId: transaction.transactionId,
+ stepId: "step-3",
+ workflowId: "long-running",
+ },
+ stepResponse: new StepResponse("Done with step 3!"),
+ })
+
+ const afterSubscriber = await workflowCompletion
+
+ expect(afterSubscriber).toBe("Finished three steps")
+ })
+ })
+ },
+})
+```
+
+In this example, you set both `step-2` and `step-3` as successful before waiting for the workflow to complete.
+
+***
+
+## Test Database Operations in Workflows
+
+In real use cases, you'll often test workflows that perform database operations, such as creating a brand.
+
+When you test such workflows, you may need to:
+
+- Verify that the database operations were performed correctly. For example, that a brand was created with the expected properties.
+- Perform database actions before testing the workflow. For example, creating a brand before testing a workflow that deletes it.
+
+This section provides examples of both scenarios.
+
+### Verify Database Operations in Workflow Test
+
+To retrieve data from the database after running a workflow, you can resolve and use either the module's service (for example, the Brand Module's service) or [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
+
+For example, the following test verifies that a brand was created by a workflow:
+
+```ts title="integration-tests/http/workflow-brand.spec.ts" highlights={workflowBrandHighlights}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import { createBrandWorkflow } from "../../src/workflows/create-brand"
+import { BRAND_MODULE } from "../../src/modules/brand"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ describe("Test create brand workflow", () => {
+ it("creates a brand", async () => {
+ const container = getContainer()
+ const { result: brand } = await createBrandWorkflow(container)
+ .run({
+ input: {
+ name: "Test Brand",
+ },
+ })
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+
+ const createdBrand = await brandModuleService.retrieveBrand(brand.id)
+ expect(createdBrand).toBeDefined()
+ expect(createdBrand.name).toBe("Test Brand")
+ })
+ })
+ },
+})
+
+jest.setTimeout(60 * 1000)
+```
+
+In this test, you run the workflow, which creates a brand. Then, you retrieve the brand from the database using the Brand Module's service and verify that it was created with the expected properties.
+
+### Perform Database Actions Before Testing Workflow
+
+You can perform database actions before testing workflows in the `beforeAll` or `beforeEach` hooks of your test suite. In those hooks, you can create data that is useful for your workflow tests.
+
+Learn more about test hooks in [Jest's Documentation](https://jestjs.io/docs/setup-teardown).
+
+You can perform the database actions before testing a workflow by either:
+
+- Using the module's service (for example, the Brand Module's service).
+- Using an existing workflow that performs the database actions.
+
+#### Use Module's Service
+
+For example, the following test creates a brand using the Brand Module's service before running the workflow that deletes it:
+
+```ts title="integration-tests/http/workflow-brand-delete.spec.ts"
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"
+import { BRAND_MODULE } from "../../src/modules/brand"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ let brandId: string
+
+ beforeAll(async () => {
+ const container = getContainer()
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+
+ const brand = await brandModuleService.createBrands({
+ name: "Test Brand",
+ })
+
+ brandId = brand.id
+ })
+
+ describe("Test delete brand workflow", () => {
+ it("deletes a brand", async () => {
+ const container = getContainer()
+ const { result } = await deleteBrandWorkflow(container)
+ .run({
+ input: {
+ id: brandId,
+ },
+ })
+
+ expect(result.success).toBe(true)
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+ await expect(brandModuleService.retrieveBrand(brandId))
+ .rejects.toThrow()
+ })
+ })
+ },
+})
+```
+
+In this example, you:
+
+1. Use the `beforeAll` hook to create a brand before running the workflow that deletes it.
+2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand.
+3. Verify that the brand was deleted successfully by checking that retrieving it throws an error.
+
+#### Use Existing Workflow
+
+Alternatively, if you already have a workflow that performs the database operations, you can use that workflow in the `beforeAll` or `beforeEach` hook. This is useful if the database operations are complex and are already encapsulated in a workflow.
+
+For example, you can modify the `beforeAll` hook to use the `createBrandWorkflow`:
+
+```ts title="integration-tests/http/workflow-brand-delete.spec.ts" highlights={workflowBrandDeleteHighlights2}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
+import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"
+import { createBrandWorkflow } from "../../src/workflows/create-brand"
+import { BRAND_MODULE } from "../../src/modules/brand"
+
+medusaIntegrationTestRunner({
+ testSuite: ({ getContainer }) => {
+ let brandId: string
+
+ beforeAll(async () => {
+ const container = getContainer()
+
+ const { result: brand } = await createBrandWorkflow(container)
+ .run({
+ input: {
+ name: "Test Brand",
+ },
+ })
+
+ brandId = brand.id
+ })
+
+ describe("Test delete brand workflow", () => {
+ it("deletes a brand", async () => {
+ const container = getContainer()
+ const { result } = await deleteBrandWorkflow(container)
+ .run({
+ input: {
+ id: brandId,
+ },
+ })
+
+ expect(result.success).toBe(true)
+
+ const brandModuleService = container.resolve(BRAND_MODULE)
+ await expect(brandModuleService.retrieveBrand(brandId))
+ .rejects.toThrow()
+ })
+ })
+ },
+})
+```
+
+In this example, you:
+
+1. Use the `beforeAll` hook to run the `createBrandWorkflow`, which creates a brand before running the workflow that deletes it.
+2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand.
+3. Verify that the brand was deleted successfully by checking that retrieving it throws an error.
+
# Write Tests for Modules