# Testing

URL: https://docs.domainruntime.dev/docs/environments/testing
Status: Guide
Reviewed: 2026-09-22



<div className="docs-note">
  Unit tests work today with any test runner. The end-to-end helpers exist in 

  `@domainruntime/testing`

   and run in our CI; they are 

  **not yet published**

   to npm.
</div>

Test at two levels:

|                | Runs                                | Checks                                           | Speed        |
| -------------- | ----------------------------------- | ------------------------------------------------ | ------------ |
| **Unit**       | your `execute`, with a fake runtime | your logic: decisions, what gets written, errors | milliseconds |
| **End to end** | your domain in a real environment   | schema, permissions, contracts, live queries     | seconds      |

## Unit: call `execute` directly [#unit-call-execute-directly]

Every action is reachable from the domain, so a test calls it with the input and a runtime you control:

```ts title="src/domain.test.ts"
import { describe, expect, it, vi } from "vitest";
import orders from "./domain";

function fakeRuntime(actor: { subject: string } | null) {
  const writes: unknown[] = [];
  const db = {
    tx: new Proxy({}, { get: (_, entity) => new Proxy({}, { get: (_, id) => ({ update: (v) => ({ entity, id, v }) }) }) }),
    transact: vi.fn(async (ops) => { writes.push(ops); }),
    query: vi.fn(async () => ({ orders_order: [] })),
  };
  return { writes, runtime: { env: { actor }, db: async () => db } };
}

describe("orders.place", () => {
  it("rejects anonymous callers before writing", async () => {
    const { runtime, writes } = fakeRuntime(null);
    await expect(
      orders.actions.place.execute({ input: { orderId: crypto.randomUUID(), total: 10 }, runtime }),
    ).rejects.toThrow("Sign in");
    expect(writes).toHaveLength(0);
  });

  it("validates its contract", () => {
    expect(() => orders.actions.place.input.parse({ orderId: "x", total: -1 })).toThrow();
  });
});
```

Calling `execute` directly skips the runtime: input is not parsed and permissions are not applied. Test the contract with `action.input.parse` and `action.output.parse`, and permissions end to end. With the planned verbs the fake runtime becomes `{ auth, query, tx }`.

## End to end: a real environment [#end-to-end-a-real-environment]

Permissions, uniqueness and live queries are only faithful on the real runtime. Create a disposable environment, publish your domain, act as a user, and destroy it:

```ts title="e2e/orders.test.ts"
import { createEnvironment, configFromEnv } from "@domainruntime/testing/e2e";
import orders from "../src/domain";

const env = await createEnvironment(configFromEnv());
try {
  await env.pushDomain(orders);
  // call actions and read as a minted user
} finally {
  await env.destroy();
}
```

## What to test where [#what-to-test-where]

* **Decisions** — who may, what is rejected, what gets computed: unit.
* **Idempotency** — a second call with the same unique key updates instead of duplicating: end to end, because it depends on `.unique()` in the real database.
* **Permissions** — end to end, as two different users, against a production-mode release (development runs actions as an administrator today).
* **Workflows** — unit-test each step function; the whole run end to end.
