From 1c1e1c6aa29eca9916f47f7d7fabe3c8736d9444 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 30 Jul 2025 17:25:46 +0300 Subject: [PATCH] docs: improved workflows integration tests guide (#13090) --- .../debug-workflows/page.mdx | 33 +- .../integration-tests/workflows/page.mdx | 456 +++++++++++++++++- www/apps/book/generated/edit-dates.mjs | 4 +- www/apps/book/public/llms-full.txt | 437 +++++++++++++++-- 4 files changed, 867 insertions(+), 63 deletions(-) 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