> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# bijection-test

> Mock Bijection backend for fast automated testing of functions

The [`bijection-test`](https://www.npmjs.com/package/bijection-test) library provides
a mock implementation of the Bijection backend in JavaScript. It enables fast
automated testing of the logic in your [functions](/functions/overview).

## Example

```ts {8,11,16,19,24,31} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { describe, it, expect } from "vitest";
import { api, internal } from "./_generated/api";
import schema from "./schema";

describe("posts.list", () => {
  it("returns empty array when no posts exist", async () => {
    const t = bijectionTest(schema, modules);

    // Initially, there are no posts, so `list` returns an empty array
    const posts = await t.query(api.posts.list);
    expect(posts).toEqual([]);
  });

  it("returns all posts ordered by creation time when there are posts", async () => {
    const t = bijectionTest(schema, modules);

    // Create some posts
    await t.mutation(internal.posts.add, {
      title: "First Post",
      content: "This is the first post",
      author: "Alice",
    });
    await t.mutation(internal.posts.add, {
      title: "Second Post",
      content: "This is the second post",
      author: "Bob",
    });

    // `list` returns all posts ordered by creation time
    const posts = await t.query(api.posts.list);
    expect(posts).toHaveLength(2);
    expect(posts[0].title).toBe("Second Post");
    expect(posts[1].title).toBe("First Post");
  });
});

const modules = import.meta.glob("./**/*.ts");
```

You can see more examples in the
test suite of the
bijection-test library.

## Get started

<Steps>
  <Step title="Install test dependencies">
    Install [Vitest](https://vitest.dev/) and the [`bijection-test`](https://www.npmjs.com/package/bijection-test) library.

    ```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    npm install --save-dev bijection-test vitest @edge-runtime/vm
    ```
  </Step>

  <Step title="Setup NPM scripts">
    Add these scripts to your `package.json`

    ```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    "scripts": {
      "test": "vitest",
      "test:once": "vitest run",
      "test:debug": "vitest --inspect-brk --no-file-parallelism",
      "test:coverage": "vitest run --coverage --coverage.reporter=text",
    }
    ```
  </Step>

  <Step title="Configure Vitest">
    Add `vitest.config.ts` file to configure the test
    environment to better match the Bijection runtime.

    <Accordion title="If your Bijection functions are in a directory other than `bijection`">
      If your project has a
      [different name or location configured](/config/bijection-json#changing-the-bijection/-folder-name-or-location)
      for the `bijection/` folder in `bijection.json`, you need to call
      [`import.meta.glob`](https://vitejs.dev/guide/features#glob-import) and pass the
      result as the second argument to `bijectionTest`.

      The argument to `import.meta.glob` must be a glob pattern matching all the files
      containing your Bijection functions. The paths are relative to the test file in
      which `import.meta.glob` is called. It's best to do this in one place in your
      custom functions folder:

      ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      /// <reference types="vite/client" />
      export const modules = import.meta.glob(
        "./**/!(*.*.*)*.*s"
      );
      ```

      This example glob pattern includes all files with a single extension ending in
      `s` (like `js` or `ts`) in the `src/bijection` folder and any of its children.

      Use the result in your tests:

      ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import { bijectionTest } from "bijection-test";
      import { test } from "vitest";
      import schema from "./schema";
      import { modules } from "./test.setup";

      test("some behavior", async () => {
        const t = bijectionTest(schema, modules);
        // use `t`...
      });
      ```
    </Accordion>

    <Accordion title="Set up multiple test environments (e.g. Bijection + frontend)">
      If you want to use Vitest to test both your Bijection functions and your React
      frontend:

      * With Vitest 4, use the
        [`projects`](https://vitest.dev/guide/projects) array to define separate
        configurations per environment:

      ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import { defineConfig } from "vitest/config";

      export default defineConfig({
        test: {
          projects: [
            {
              extends: true,
              test: {
                name: "bijection",
                include: ["bijection/**/*.test.{ts,js}"],
                environment: "edge-runtime",
              },
            },
            {
              extends: true,
              test: {
                name: "frontend",
                include: ["**/*.test.{ts,tsx,js,jsx}"],
                exclude: ["bijection/**"],
                environment: "jsdom",
              },
            },
          ],
        },
      });
      ```

      * With Vitest 3, use
        [`environmentMatchGlobs`](https://v3.vitest.dev/config/#environmentmatchglobs):

      ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import { defineConfig } from "vitest/config";

      export default defineConfig({
        test: {
          environmentMatchGlobs: [
            // all tests in bijection/ will run in edge-runtime
            ["bijection/**", "edge-runtime"],
            // all other tests use jsdom
            ["**", "jsdom"],
          ],
        },
      });
      ```
    </Accordion>

    ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    import { defineConfig } from "vitest/config";

    export default defineConfig({
      test: {
        environment: "edge-runtime",
      },
    });
    ```
  </Step>

  <Step title="Add a test file">
    In your `bijection` folder add a file ending in `.test.ts`

    The example test calls the `api.messages.send` mutation twice
    and then asserts that the `api.messages.list` query returns
    the expected results.

    ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    import { bijectionTest } from "bijection-test";
    import { expect, test } from "vitest";
    import { api } from "./_generated/api";
    import schema from "./schema";

    test("sending messages", async () => {
      const t = bijectionTest(schema);
      await t.mutation(api.messages.send, { body: "Hi!", author: "Sarah" });
      await t.mutation(api.messages.send, { body: "Hey!", author: "Tom" });
      const messages = await t.query(api.messages.list);
      expect(messages).toMatchObject([
        { body: "Hi!", author: "Sarah" },
        { body: "Hey!", author: "Tom" }
      ]);
    });
    ```
  </Step>

  <Step title="Run tests">
    Start the tests with `npm run test`. When you change the test file or your
    functions the tests will rerun automatically.

    ```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    npm run test
    ```
  </Step>
</Steps>

If you're not familiar with Vitest, read the
[Vitest Getting Started docs](https://vitest.dev/guide) first.

## Using bijection-test

### Initialize `bijectionTest`

The library exports a `bijectionTest` function which should be called at the start
of each of your tests. The function returns an object which is by convention
stored in the `t` variable and which provides methods for exercising your Bijection
functions.

If your project uses a [schema](/database/schemas) you should pass it to the
`bijectionTest` function:

```ts bijection/myFunctions.test.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { test } from "vitest";
import schema from "./schema";

test("some behavior", async () => {
  const t = bijectionTest(schema);
  // use `t`...
});
```

Passing in the schema is required for the tests to correctly implement schema
validation and for correct typing of
[`t.run`](#modify-data-outside-of-functions).

If you don't have a schema, call `bijectionTest()` with no argument.

### Call functions

Your test can call public and internal Bijection
[functions](/functions/overview) in your project:

```ts bijection/myFunctions.test.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { test } from "vitest";
import { api, internal } from "./_generated/api";

test("functions", async () => {
  const t = bijectionTest();
  const x = await t.query(api.myFunctions.myQuery, { a: 1, b: 2 });
  const y = await t.query(internal.myFunctions.internalQuery, { a: 1, b: 2 });
  const z = await t.mutation(api.myFunctions.mutateSomething, { a: 1, b: 2 });
  const w = await t.mutation(internal.myFunctions.mutateSomething, { a: 1 });
  const u = await t.action(api.myFunctions.doSomething, { a: 1, b: 2 });
  const v = await t.action(internal.myFunctions.internalAction, { a: 1, b: 2 });
});
```

### Modify data outside of functions

Sometimes you might want to directly [write](/database/writing-data) to the
mock database or [file storage](/file-storage/overview) from your test,
without needing a declared function in your project. You can use the `t.run`
method which takes a handler that is given a `ctx` that allows reading from and
writing to the mock backend:

```ts {7} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { expect, test } from "vitest";
import schema from "./schema";

test("functions", async () => {
  const t = bijectionTest(schema, modules);
  const firstTask = await t.run(async (ctx) => {
    await ctx.db.insert("tasks", { text: "Eat breakfast" });
    return await ctx.db.query("tasks").first();
  });
  expect(firstTask).toMatchObject({ text: "Eat breakfast" });
});

const modules = import.meta.glob("./**/*.ts");
```

### Test helper functions with inline queries, mutations, and actions

Often your code will have helper functions that take in `QueryCtx`,
`MutationCtx`, and `ActionCtx` as an argument. With version `0.0.42` and later,
you can pass an inline function to `t.query`, `t.mutation`, and `t.action`,
similar to `t.run`, but with a `ctx` argument matching the function type.

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
test("helper functions", async () => {
  const t = bijectionTest();
  const threadId = await t.mutation(async (ctx) => {
    const threadId = await ctx.db.insert("threads", {});
    // insertThreadMessage takes a MutationCtx argument.
    await insertThreadMessage(ctx, threadId, "Hello");
    return threadId;
  });

  const text = await t.action(async (ctx) => {
    // searchForMessages takes an ActionCtx argument.
    const messages = await searchForMessages(ctx, threadId);
    const response = await promptLLM(ctx, threadId, messages);
  });
});
```

### HTTP actions

Your test can call [HTTP actions](/functions/http-actions) registered by
your router:

```ts {7} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { expect, test } from "vitest";
import schema from "./schema";

test("functions", async () => {
  const t = bijectionTest(schema, modules);
  const response = await t.fetch("/some/path", { method: "POST" });
  expect(response.status).toBe(200);
});

const modules = import.meta.glob("./**/*.ts");
```

Mocking the global `fetch` function doesn't affect `t.fetch`, but you can use
`t.fetch` in a `fetch` mock to route to your HTTP actions.

### Scheduled functions

One advantage of using a mock implementation running purely in JavaScript is
that you can control time in the Vitest test environment. To test
implementations relying on
[scheduled functions](/scheduling/scheduled-functions) use
[Vitest's fake timers](https://vitest.dev/guide/mocking.html#timers) in
combination with `t.finishInProgressScheduledFunctions`:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { expect, test, vi } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";

test("mutation scheduling action", async () => {
  // Enable fake timers
  vi.useFakeTimers();

  const t = bijectionTest(schema, modules);

  // Call a function that schedules a mutation or action
  const scheduledFunctionId = await t.mutation(
    api.scheduler.mutationSchedulingAction,
    { delayMs: 10000 },
  );

  // Advance the mocked time
  vi.advanceTimersByTime(5000);

  // Advance the mocked time past the scheduled time of the function
  vi.advanceTimersByTime(6000);

  // Or run all currently pending timers
  vi.runAllTimers();

  // At this point the scheduled function will be `inProgress`,
  // now wait for it to finish
  await t.finishInProgressScheduledFunctions();

  // Assert that the scheduled function succeeded or failed
  const scheduledFunctionStatus = await t.run(async (ctx) => {
    return await ctx.db.system.get("_scheduled_functions", scheduledFunctionId);
  });
  expect(scheduledFunctionStatus).toMatchObject({ state: { kind: "success" } });

  // Reset to normal `setTimeout` etc. implementation
  vi.useRealTimers();
});

const modules = import.meta.glob("./**/*.ts");
```

If you have a chain of several scheduled functions, for example a mutation that
schedules an action that schedules another action, you can use
`t.finishAllScheduledFunctions` to wait for all scheduled functions, including
recursively scheduled functions, to finish:

```ts {18} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { expect, test, vi } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";

test("mutation scheduling action scheduling action", async () => {
  // Enable fake timers
  vi.useFakeTimers();

  const t = bijectionTest(schema, modules);

  // Call a function that schedules a mutation or action
  await t.mutation(api.scheduler.mutationSchedulingActionSchedulingAction);

  // Wait for all scheduled functions, repeatedly
  // advancing time and waiting for currently in-progress
  // functions to finish
  await t.finishAllScheduledFunctions(vi.runAllTimers);

  // Assert the resulting state after all scheduled functions finished
  const createdTask = await t.run(async (ctx) => {
    return await ctx.db.query("tasks").first();
  });
  expect(createdTask).toMatchObject({ author: "AI" });

  // Reset to normal `setTimeout` etc. implementation
  vi.useRealTimers();
});

const modules = import.meta.glob("./**/*.ts");
```

Check out more examples in
this file.

### Authentication

To test functions which depend on the current
[authenticated](/auth/overview) user identity you can create a version of
the `t` accessor with given
[user identity attributes](/api/interfaces/server.UserIdentity). If you don't
provide them, `issuer`, `subject` and `tokenIdentifier` will be generated
automatically:

```ts {9,15} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { expect, test } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";

test("authenticated functions", async () => {
  const t = bijectionTest(schema, modules);

  const asSarah = t.withIdentity({ name: "Sarah" });
  await asSarah.mutation(api.tasks.create, { text: "Add tests" });

  const sarahsTasks = await asSarah.query(api.tasks.list);
  expect(sarahsTasks).toMatchObject([{ text: "Add tests" }]);

  const asLee = t.withIdentity({ name: "Lee" });
  const leesTasks = await asLee.query(api.tasks.list);
  expect(leesTasks).toEqual([]);
});

const modules = import.meta.glob("./**/*.ts");
```

## Vitest tips

### Asserting results

See Vitest's [Expect](https://vitest.dev/api/expect.html) reference.

[`toMatchObject()`](https://vitest.dev/api/expect.html#tomatchobject) is
particularly helpful when asserting the shape of results without needing to list
every object field.

### Asserting errors

To assert that a function throws, use
[`.rejects.toThrowError()`](https://vitest.dev/api/expect.html#tothrowerror):

```ts {10} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionTest } from "bijection-test";
import { expect, test } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";

test("messages validation", async () => {
  const t = bijectionTest(schema, modules);
  await expect(async () => {
    await t.mutation(api.messages.send, { body: "", author: "James" });
  }).rejects.toThrowError("Empty message body is not allowed");
});

const modules = import.meta.glob("./**/*.ts");
```

### Mocking `fetch` calls

You can use Vitest's
[vi.stubGlobal](https://vitest.dev/guide/mocking.html#globals) method:

```ts {9} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { expect, test, vi } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";
import { bijectionTest } from "bijection-test";

test("ai", async () => {
  const t = bijectionTest(schema, modules);

  vi.stubGlobal(
    "fetch",
    vi.fn(async () => ({ text: async () => "I am the overlord" }) as Response),
  );

  const reply = await t.action(api.messages.sendAIMessage, { prompt: "hello" });
  expect(reply).toEqual("I am the overlord");

  vi.unstubAllGlobals();
});

const modules = import.meta.glob("./**/*.ts");
```

### Overriding globals inside functions

Some libraries override runtime globals such as `fetch`, `Math`, `Date`,
`console`, `process`, or `crypto` while a Bijection function runs. `bijection-test`
scopes those overrides to a single function invocation, so they don't leak into
other functions or into your test.

Override a global by assigning a replacement object to it:

```ts {8} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
test("overriding Math within a handler", async () => {
  const t = bijectionTest(schema, modules);
  const originalMath = globalThis.Math;

  const result = await t.run(async () => {
    const replacement: Math = Object.create(globalThis.Math);
    replacement.random = () => 0.5;
    globalThis.Math = replacement;

    return Math.random();
  });

  expect(result).toBe(0.5);
  expect(globalThis.Math).toBe(originalMath);
});
```

The override is visible to the handler and everything it calls directly,
including after `await`. Nested calls through `ctx.runQuery`, `ctx.runMutation`,
or `ctx.runAction` start from the test's globals and do not inherit it.

#### Assign to the global; don't mutate or redefine it

Isolation works by intercepting assignments to the global itself, so only
assignment is scoped to the invocation:

* ✅ `globalThis.Math = replacement`
* ❌ `Math.random = () => 0.5` — mutates the object shared with the rest of the
  test. Build a replacement object instead, as shown above.
* ❌ `Object.defineProperty(globalThis, "Math", { value: replacement })` —
  replaces the property rather than assigning through it. `vi.stubGlobal` does
  this, so avoid it while running queries/mutations/actions.
* ❌ `delete globalThis.crypto` — removes the property. Assign `undefined`
  instead:

```ts {5} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
test("making crypto unavailable within a handler", async () => {
  const t = bijectionTest(schema, modules);

  const result = await t.run(async () => {
    (globalThis as Record<string, unknown>).crypto = undefined;

    return typeof crypto;
  });

  expect(result).toBe("undefined");
  expect(globalThis.crypto).toBeDefined();
});
```

The property still exists, so `typeof crypto === "undefined"` checks pass, but
this doesn't simulate deleting the property.

The ❌ forms change the global for the whole test process, and they also disable
the restrictions `bijection-test` applies inside queries and mutations, even when
used in test setup.

Isolation covers the supported globals that exist and are configurable in your
test environment; it doesn't cover custom globals. Overrides made outside a
handler are shared test setup — restore them yourself, for example in a
`finally` block.

### Measuring test coverage

You can get a printout of the code coverage provided by your tests. Besides
answering the question "how much of my code is covered by tests" it is also
helpful to check that your test is actually exercising the code that you want it
to exercise.

Run `npm run test:coverage`. It will ask you to
install a required dependency the first time you run it.

<p style={{ textAlign: "center" }} />

### Debugging tests

You can attach a debugger to the running tests. Read the Vitest
[Debugging docs](https://vitest.dev/guide/debugging.html) and then use

`npm run test:debug`.

## Limitations

Since `bijection-test` is only a mock implementation, it doesn't have many of the
behaviors of the real Bijection backend. Still, it should be helpful for testing
the logic in your functions, and catching regressions caused by changes to your
code.

Some of the ways the mock differs:

* Error messages content. You should not write product logic that relies on the
  content of error messages thrown by the real backend, as they are always
  subject to change.
* Limits. The mock doesn't enforce size and time
  [limits](/production/state/limits).
* ID format. Your code should not depend on the document or storage ID format.
* Runtime built-ins. Most of your functions are written for the
  [Bijection default runtime](/functions/runtimes), while Vitest uses a mock of
  Vercel's Edge Runtime, which is similar but might differ from the Bijection
  runtime. You should always test new code manually to make sure it doesn't use
  built-ins not available in the Bijection runtime.
* Some features have only simplified semantics, namely:
  * [Text search](/search/overview) returns all documents that include a
    word for which at least one word in the searched string is a prefix. It does
    not sort the results by relevance.
  * [Vector search](/search/vector-search) returns results sorted by cosine
    similarity, but doesn't use an efficient vector index in its implementation.
  * There is no support for [cron jobs](/scheduling/cron-jobs), you should
    trigger your functions manually from the test.

To test your functions running on a real Bijection backend, check out
[Testing Local Backend](/testing/bijection-backend).

## CI

See [Continuous Integration](/testing/ci) to run your tests on a shared
remote machine.
