diff --git a/pages/docs/reference/testing/index.mdx b/pages/docs/reference/testing/index.mdx new file mode 100644 index 000000000..f2fab01b6 --- /dev/null +++ b/pages/docs/reference/testing/index.mdx @@ -0,0 +1,308 @@ +import { Callout, CodeGroup } from "shared/Docs/mdx"; + +# Testing + +To test your Inngest functions programmatically, use the `@inngest/test` +library, available on [npm](https://www.npmjs.com/package/@inngest/test) and [JSR](https://jsr.io/@inngest/test). + +This allows you to mock function state, step tooling, and inputs with a +Jest-compatible API supporting all major testing frameworks, runtimes, and +libraries: + +- `jest` +- `vitest` +- `bun:test` (Bun) +- `@std/expect` (Deno) +- `chai`/`expect` + +## Installation + +The `@inngest/test` package requires `inngest@>=3.22.12`. + + +```shell {{ title: "npm" }} +npm install -D @inngest/test +``` +```shell {{ title: "Yarn" }} +yarn add -D @inngest/test +``` +```shell {{ title: "pnpm" }} +pnpm add -D @inngest/test +``` +```shell {{ title: "Bun" }} +bun add -d @inngest/test +``` +```shell {{ title: "Deno" }} +deno add --dev @inngest/test +# or with JSR... +deno add --dev jsr:@inngest/test +``` + + +## Unit tests + +Use whichever supported testing framework; `@inngest/test` is unopinionated +about how your tests are run. We'll demonstrate here using `jest`. + +Import `InngestTestEngine`, our function to test, and create a new +`InngestTestEngine` instance. + +```ts +import { InngestTestEngine } from "@inngest/test"; +import { helloWorld } from "./helloWorld"; + +describe("helloWorld function", () => { + const t = new InngestTestEngine({ + function: helloWorld, + }); +}); +``` + +Now we can use the primary API for testing, `t.execute()`: + +```ts +test("returns a greeting", async () => { + const { result } = await t.execute(); + expect(result).toEqual("Hello World!"); +}); +``` + +This will run the entire function (steps and all) to completion, then return the +response from the function, where we assert that it was the string `"Hello +World!"`. + +An serialized `error` will be returned instead of `result` if the function threw: + +```ts +test("throws an error", async () => { + const { error } = await t.execute(); + expect(error).toContain("Some specific error"); +}); +``` + +### Running an individual step + +`t.executeStep()` can be used to run the function until a particular step has +been executed. + +This is useful to test a single step within a function or to see that a +non-runnable step such as `step.waitForEvent()` has been registered with the +correct options. + +```ts +test("runs the price calculations", async () => { + const { result } = await t.executeStep("calculate-price"); + expect(result).toEqual(123); +}); +``` + +Assertions can also be made on steps in any part of a run, regardless of if +that's the checkpoint we've waited for. See [Assertions -> State](#assertions). + +### Assertions + +`@inngest/test` adds Jest-compatible mocks by default that can help you assert +function and step input and output. You can assert: + +- Function input +- Function output +- Step output +- Step tool usage + +All of these values are returned from both `t.execute()` and `t.executeStep()`; +we'll only show one for simplicit here. + +The `result` is returned, which is the output of the run or step: + +```ts +const { result } = await t.execute(); +expect(result).toEqual("Hello World!"); +``` + +`ctx` is the input used for the function run. This can be used to assert outputs +that are based on input data such as `event` or `runId`, or to confirm that +middleware is working correctly and affecting input arguments. + +```ts +const { ctx, result } = await t.execute(); +expect(result).toEqual(`Run ID was: "${ctx.runId}"`); +``` + +The step tooling at `ctx.step` are all Jest-compatible spy functions, so you can +use them to assert that they've been called and used correctly: + +```ts +const { ctx } = await t.execute(); +expect(ctx.step.run).toHaveBeenCalledWith("my-step", expect.any(Function)); +``` + +`state` is also returned, which is a view into the outputs of all steps in the +run. This allows you to test each individual step output for any given input: + +```ts +const { state } = await t.execute(); +expect(state["my-step"]).resolves.toEqual("some successful output"); +expect(state["dangerous-step"]).rejects.toThrowError("something failed"); +``` + +### Mocking + +Some mocking is done automatically by `@inngest/test`, but can be overwritten if +needed. + +All mocks detailed below can be specified either when creating an +`InngestTestEngine` instance or for each individual execution: + +```ts +// Set the events for every execution +const t = new InngestTestEngine({ + function: helloWorld, + // mocks here +}); + +// Or for just one, which will overwrite any current event mocks +t.execute({ + // mocks here +}); + +t.executeStep("my-step", { + // mocks here +}) +``` + +You can also clone an existing `InngestTestEngine` instance to encourage re-use +of complex mocks: + +```ts +// Make a direct clone, which includes any mocks +const otherT = t.clone(); + +// Provide some more mocks in addition to any existing ones +const anotherT = t.clone({ + // mocks here +}); +``` + +For simplicity, the following examples will show usage of `t.execute()`, but the +mocks can be placed in any of these locations. + +#### Events + +The incoming event data can be mocked. They are always specified as an array of +events to allow also mocking batches. + +```ts +t.execute({ + events: [{ name: "demo/event.sent", data: { message: "Hi!" } }], +}); +``` + + +If no event mocks are given at all (or `events: undefined` is explicitly set), +an `inngest/function.invoked` event will be mocked for you. + + +#### Steps + +Mocking steps can help you model different paths and situations within your +function. To do so, any step can be mocked by providing the `steps` option. + +Here we mock two steps, one that will run successfully and another that will +model a failure and throw an error: + +```ts +t.execute({ + steps: [ + { + id: "successful-step", + handler() { + return "We did it!"; + }, + }, + { + id: "dangerous-step", + handler() { + throw new Error("Oh no!"); + }, + }, + ], +}); +``` + +These handlers will lazily when they rae found during a function's execution. +This means you can write complex mocks that respond to other information: + +```ts +let message = ""; + +t.execute({ + steps: [ + { + id: "build-greeting", + handler() { + message = "Hello, "; + return message; + }, + }, + { + id: "build-name", + handler() { + return message + " World!"; + }, + }, + ], +}); +``` + +#### Modules and imports + +Any mocking of modules or imports outside of Inngest which your functions may +rely on should be done outside of Inngest with the testing framework you're +using. + +Here are some links to the major supported frameworks and their guidance for +mocking imports: + +- [`jest`](https://jestjs.io/docs/mock-functions#mocking-modules) +- [`vitest`](https://vitest.dev/guide/mocking#modules) +- [`bun:test` (Bun)](https://bun.sh/docs/test/mocks#module-mocks-with-mock-module) +- [`@std/testing` (Deno)](https://jsr.io/@std/testing/doc/mock/~) + +#### Custom + +You can also provide your own custom mocks for the function input. + +When instantiating a new `InngestTestEngine` or starting an execution, provide a +`transformCtx` function that will add these mocks every time the function is +run: + +```ts +const t = new InngestTestEngine({ + function: helloWorld, + transformCtx: (ctx) => { + return { + ...ctx, + event: someCustomThing, + }; + }, +}); +``` + +If you wish to still add the automatic mocking from `@inngest/test` (such as the +spies on `ctx.step.*`), you can import and use the automatic transforms as part +of your own: + +```ts +import { InngestTestEngine, mockCtx } from "@inngest/test"; + +const t = new InngestTestEngine({ + function: helloWorld, + transformCtx: (ctx) => { + return { + ...mockCtx(ctx), + event: someCustomThing, + }; + }, +}); +``` + diff --git a/shared/Docs/navigationStructure.ts b/shared/Docs/navigationStructure.ts index d51800aeb..b05014759 100644 --- a/shared/Docs/navigationStructure.ts +++ b/shared/Docs/navigationStructure.ts @@ -115,6 +115,10 @@ const sectionReference: NavGroup[] = [ title: "Referencing functions", href: `/docs/functions/references`, }, + { + title: "Testing", + href: "/docs/reference/testing", + }, { title: "Steps", links: [