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: [