-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1771399
commit 80c88e3
Showing
2 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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`. | ||
|
||
<CodeGroup> | ||
```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 | ||
``` | ||
</CodeGroup> | ||
|
||
## 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!" } }], | ||
}); | ||
``` | ||
|
||
<Callout> | ||
If no event mocks are given at all (or `events: undefined` is explicitly set), | ||
an `inngest/function.invoked` event will be mocked for you. | ||
</Callout> | ||
|
||
#### 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, | ||
}; | ||
}, | ||
}); | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters