7.2.2. Example: Write Integration Tests for Workflows

In this chapter, you'll learn how to write integration tests for workflows using medusaIntegrationTestRunner from Medusa's Testing Framework.

Note: For other debugging approaches, refer to the Debug Workflows chapter.

Write Integration Test for a Workflow#

Consider you have the following workflow defined at src/workflows/hello-world.ts:

src/workflows/hello-world.ts
1import {2  createWorkflow,3  createStep,4  StepResponse,5  WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7
8const step1 = createStep("step-1", () => {9  return new StepResponse("Hello, World!")10})11
12export const helloWorldWorkflow = createWorkflow(13  "hello-world-workflow",14  () => {15    const message = step1()16
17    return new WorkflowResponse(message)18  }19)

To write a test for this workflow, create the file integration-tests/http/workflow.spec.ts with the following content:

integration-tests/http/workflow.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import { helloWorldWorkflow } from "../../src/workflows/hello-world"3
4medusaIntegrationTestRunner({5  testSuite: ({ getContainer }) => {6    describe("Test hello-world workflow", () => {7      it("returns message", async () => {8        const { result } = await helloWorldWorkflow(getContainer())9          .run()10
11        expect(result).toEqual("Hello, World!")12      })13    })14  },15})16
17jest.setTimeout(60 * 1000)

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#

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

integration-tests/http/custom-routes.spec.ts
1// in your test's file2jest.setTimeout(60 * 1000)

Run Tests#

Run the following command to run your tests:

Tip: If you don't have a test:integration:http script in package.json, refer to the Medusa Testing Tools chapter.

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 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 the following step in your workflow that throws a MedusaError:

src/workflows/hello-world.ts
1import { MedusaError } from "@medusajs/framework/utils"2import { createStep } from "@medusajs/framework/workflows-sdk"3
4const step1 = createStep("step-1", () => {5  throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist")6})

You can write the following test to ensure that the workflow throws that error:

integration-tests/http/workflow.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import { helloWorldWorkflow } from "../../src/workflows/hello-world"3
4medusaIntegrationTestRunner({5  testSuite: ({ getContainer }) => {6    describe("Test hello-world workflow", () => {7      it("should throw error when item doesn't exist", async () => {8        const { errors } = await helloWorldWorkflow(getContainer())9          .run({10            throwOnError: false,11          })12
13        expect(errors.length).toBeGreaterThan(0)14        expect(errors[0].error.message).toBe("Item doesn't exist")15      })16    })17  },18})19
20jest.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, 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 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:

src/workflows/long-running-workflow.ts
1import { 2  createStep,  3  createWorkflow,4  WorkflowResponse,5  StepResponse,6} from "@medusajs/framework/workflows-sdk"7
8const step1 = createStep("step-1", async () => {9  return new StepResponse({})10})11
12const step2 = createStep(13  {14    name: "step-2",15    async: true,16  },17  async () => {18    console.log("Waiting to be successful...")19  }20)21
22const step3 = createStep("step-3", async () => {23  return new StepResponse("Finished three steps")24})25
26const longRunningWorkflow = createWorkflow(27  "long-running", 28  function () {29    step1()30    step2()31    const message = step3()32
33    return new WorkflowResponse({34      message,35    })36  }37)38
39export 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:

integration-tests/http/long-running-workflow.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import longRunningWorkflow from "../../src/workflows/long-running-workflow"3import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"4import { StepResponse } from "@medusajs/framework/workflows-sdk"5
6medusaIntegrationTestRunner({7  testSuite: ({ getContainer }) => {8    describe("Test long-running workflow", () => {9      it("returns message", async () => {10        const container = getContainer()11        const { transaction } = await longRunningWorkflow(container)12          .run()13
14        const workflowEngineService = container.resolve(15          Modules.WORKFLOW_ENGINE16        )17
18        let workflowOk: any19        const workflowCompletion = new Promise((ok) => {20          workflowOk = ok21        })22
23        const subscriptionOptions = {24          workflowId: "long-running",25          transactionId: transaction.transactionId,26          subscriberId: "long-running-subscriber",27        }28
29
30        await workflowEngineService.subscribe({31          ...subscriptionOptions,32          subscriber: async (data) => {33            if (data.eventType === "onFinish") {34              workflowOk(data.result.message)35              // unsubscribe36              await workflowEngineService.unsubscribe({37                ...subscriptionOptions,38                subscriberOrId: subscriptionOptions.subscriberId,39              })40            }41          },42        })43
44        await workflowEngineService.setStepSuccess({45          idempotencyKey: {46            action: TransactionHandlerType.INVOKE,47            transactionId: transaction.transactionId,48            stepId: "step-2",49            workflowId: "long-running",50          },51          stepResponse: new StepResponse("Done!"),52        })53
54        const afterSubscriber = await workflowCompletion55
56        expect(afterSubscriber).toBe("Finished three steps")57      })58    })59  },60})61
62jest.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'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:

integration-tests/http/long-running-workflow-multiple-steps.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import longRunningWorkflow from "../../src/workflows/long-running-workflow"3import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"4import { StepResponse } from "@medusajs/framework/workflows-sdk"5
6medusaIntegrationTestRunner({7  testSuite: ({ getContainer }) => {8    describe("Test long-running workflow with multiple async steps", () => {9      it("returns message", async () => {10        const container = getContainer()11        const { transaction } = await longRunningWorkflow(container)12          .run()13
14        const workflowEngineService = container.resolve(15          Modules.WORKFLOW_ENGINE16        )17
18        let workflowOk: any19        const workflowCompletion = new Promise((ok) => {20          workflowOk = ok21        })22
23        const subscriptionOptions = {24          workflowId: "long-running",25          transactionId: transaction.transactionId,26          subscriberId: "long-running-subscriber",27        }28
29        await workflowEngineService.subscribe({30          ...subscriptionOptions,31          subscriber: async (data) => {32            if (data.eventType === "onFinish") {33              workflowOk(data.result.message)34              // unsubscribe35              await workflowEngineService.unsubscribe({36                ...subscriptionOptions,37                subscriberOrId: subscriptionOptions.subscriberId,38              })39            }40          },41        })42
43        await workflowEngineService.setStepSuccess({44          idempotencyKey: {45            action: TransactionHandlerType.INVOKE,46            transactionId: transaction.transactionId,47            stepId: "step-2",48            workflowId: "long-running",49          },50          stepResponse: new StepResponse("Done!"),51        })52
53        await workflowEngineService.setStepSuccess({54          idempotencyKey: {55            action: TransactionHandlerType.INVOKE,56            transactionId: transaction.transactionId,57            stepId: "step-3",58            workflowId: "long-running",59          },60          stepResponse: new StepResponse("Done with step 3!"),61        })62
63        const afterSubscriber = await workflowCompletion64
65        expect(afterSubscriber).toBe("Finished three steps")66      })67    })68  },69})

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.

For example, the following test verifies that a brand was created by a workflow:

integration-tests/http/workflow-brand.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import { createBrandWorkflow } from "../../src/workflows/create-brand"3import { BRAND_MODULE } from "../../src/modules/brand"4
5medusaIntegrationTestRunner({6  testSuite: ({ getContainer }) => {7    describe("Test create brand workflow", () => {8      it("creates a brand", async () => {9        const container = getContainer()10        const { result: brand } = await createBrandWorkflow(container)11          .run({12            input: {13              name: "Test Brand",14            },15          })16
17        const brandModuleService = container.resolve(BRAND_MODULE)18
19        const createdBrand = await brandModuleService.retrieveBrand(brand.id)20        expect(createdBrand).toBeDefined()21        expect(createdBrand.name).toBe("Test Brand")22      })23    })24  },25})26
27jest.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.

Tip: Learn more about test hooks in Jest's Documentation.

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:

integration-tests/http/workflow-brand-delete.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"3import { BRAND_MODULE } from "../../src/modules/brand"4
5medusaIntegrationTestRunner({6  testSuite: ({ getContainer }) => {7    let brandId: string8
9    beforeAll(async () => {10      const container = getContainer()11      12      const brandModuleService = container.resolve(BRAND_MODULE)13      14      const brand = await brandModuleService.createBrands({15        name: "Test Brand",16      })17
18      brandId = brand.id19    })20
21    describe("Test delete brand workflow", () => {22      it("deletes a brand", async () => {23        const container = getContainer()24        const { result } = await deleteBrandWorkflow(container)25          .run({26            input: {27              id: brandId,28            },29          })30
31        expect(result.success).toBe(true)32
33        const brandModuleService = container.resolve(BRAND_MODULE)34        await expect(brandModuleService.retrieveBrand(brandId))35          .rejects.toThrow()36      })37    })38  },39})

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:

integration-tests/http/workflow-brand-delete.spec.ts
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"3import { createBrandWorkflow } from "../../src/workflows/create-brand"4import { BRAND_MODULE } from "../../src/modules/brand"5
6medusaIntegrationTestRunner({7  testSuite: ({ getContainer }) => {8    let brandId: string9
10    beforeAll(async () => {11      const container = getContainer()12      13      const { result: brand } = await createBrandWorkflow(container)14        .run({15          input: {16            name: "Test Brand",17          },18        })19
20      brandId = brand.id21    })22
23    describe("Test delete brand workflow", () => {24      it("deletes a brand", async () => {25        const container = getContainer()26        const { result } = await deleteBrandWorkflow(container)27          .run({28            input: {29              id: brandId,30            },31          })32
33        expect(result.success).toBe(true)34
35        const brandModuleService = container.resolve(BRAND_MODULE)36        await expect(brandModuleService.retrieveBrand(brandId))37          .rejects.toThrow()38      })39    })40  },41})

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.
Was this chapter helpful?
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break