---
title: "Test your app"
description: "Test Pinecart with createTestApp from @mainsdk/testing - in-memory fakes, assertions, time control, chaos drills and policy tests, with no provider keys."
section: "guides"
status: "Building for 0.4"
spec: "testing-kit"
canonical: "https://mainsdk.dev/docs/guides/testing"
updated: "2026-10-28"
---

# Test your app

> Test Pinecart with createTestApp from @mainsdk/testing - in-memory fakes, assertions, time control, chaos drills and policy tests, with no provider keys.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/testing · Updated: 2026-10-28

Write tests for code that uses MainSDK and run them with no network and no provider keys. `createTestApp` from `@mainsdk/testing` puts every interface on an in-memory fake generated from the same contracts as the SDK, so a fake can't drift from production. The full API is on [Testing kit](https://mainsdk.dev/docs/interfaces/testing); this guide walks through the common tasks.

> **Note:** The testing kit ships in phase 1, milestone 0.4.

**Before you start:** a MainSDK app with a `main.yaml`, and a test runner. Node's built-in runner (`node:test`) is the example; any runner works.

## 1. Install the kit

npm:

```bash
npm install --save-dev @mainsdk/testing
```

pnpm:

```bash
pnpm add -D @mainsdk/testing
```

bun:

```bash
bun add -d @mainsdk/testing
```


## 2. Write a test with fakes

`createTestApp({ main: "./main.yaml" })` reads your `main.yaml` and puts every interface on its fake. Fakes enforce the same guarantees as production: `email.marketing()` without an unsubscribe link fails in a test too.

```ts title="tests/checkout.test.ts"
import { describe, it } from "node:test";
import { createTestApp, expect, mailbox, type TestApp } from "@mainsdk/testing";

// Your own code, from app/lib/orders.ts: it takes the app so a test can pass the fake one.
declare function placeOrder(app: TestApp, order: { email: string; amount: number; currency: string }): Promise<void>;

describe("checkout", () => {
  it("charges once and sends a receipt", async () => {
    const app = createTestApp({ main: "./main.yaml" });

    await placeOrder(app, { email: "a@pinecart.dev", amount: 4900, currency: "INR" });

    expect(app.pay).toHaveCharged({ amount: 4900, currency: "INR", times: 1 });
    mailbox(app).toHaveSent({ to: "a@pinecart.dev", template: "receipt" });
  });
});
```

## 3. Run it and verify

npm:

```bash
node --test tests/checkout.test.ts
```

pnpm:

```bash
pnpm exec node --test tests/checkout.test.ts
```


The test passes with no provider account. To see a useful failure, change `times: 1` to `times: 2`: the assertion fails and lists the one charge that did happen.

## Control time

Expiring links, TTLs and retries run without real waiting:

```ts title="tests/share.test.ts"
import { it } from "node:test";
import { createTestApp, clock } from "@mainsdk/testing";
import { share } from "mainsdk";

it("share links expire after 24 hours", async () => {
  const app = createTestApp({ main: "./main.yaml" });
  const { url } = await share({ url: "https://pinecart.dev/orders/o_1" }, { expiresIn: "24h" });
  clock.advance("25h");
  // resolving url now reports it expired
});
```

## Test policies

```ts title="tests/policy.test.ts"
import { it } from "node:test";
import { policyTest } from "@mainsdk/testing";

it("only owners update orders", () => {
  policyTest("policies/")
    .expect({
      subject: { type: "user", id: "u_2" },
      action: "orders.update",
      resource: { type: "order", id: "o_1", owner: "u_1" },
    })
    .toDecide("deny")
    .byRule("orders.owner_only");
});
```

From the terminal, `mainsdk test policy` runs your policy tests and replays recorded decisions, listing every decision a change would flip. See [Policy as code](https://mainsdk.dev/docs/concepts/policy).

## Prove your fallbacks with chaos drills

Inject an outage, added latency or a malformed response into one driver and check that the fallback in `main.yaml` takes over.

```yaml title="main.yaml"
fallback:
  email: [resend, ses]
```

```ts title="tests/fallback.test.ts"
import { it } from "node:test";
import { createTestApp, chaos, mailbox, type TestApp } from "@mainsdk/testing";

// Your own code, from app/lib/receipts.ts.
declare function sendReceipt(app: TestApp, orderId: string): Promise<void>;

it("fails over to ses when resend is down", async () => {
  const app = createTestApp({ main: "./main.yaml" });
  chaos.on("email:resend", { outage: true });

  await sendReceipt(app, "o_1");

  mailbox(app).toHaveSent({ template: "receipt", times: 1 });
});
```

With `{ latency: "3s" }` the call times out per contract and fails over; with `{ respond: "malformed" }` it raises a typed error.

## Build and test your own driver

The driver kit lets anyone build a driver for an interface. `mainsdk driver test` runs exactly the conformance suite first-party drivers pass, against a local harness with fake traffic.

```ts title="relaymail/index.ts"
import { defineDriver, type EmailMessage } from "mainsdk";

export default defineDriver({
  implements: "email@1",
  id: "relaymail",
  manifest: {
    regions: ["us", "eu", "in"],
    data: ["email-address", "message-body"],
    subprocessors: ["Relaymail Inc."],
  },
  async send(msg: EmailMessage, ctx: { http: { post(path: string, body: unknown): Promise<{ id: string }> } }) {
    const res = await ctx.http.post("/v1/send", msg);
    return { id: res.id };
  },
});
```

```bash
npx mainsdk driver test ./relaymail --interface email@1.4
```

```text
email@1.4 conformance: 86 passed, 0 failed · capability profile: attachments=no · idempotency=sdk
```

A driver that calls a host it didn't declare fails with `MAIN-NET-0002`. `defineDriver` comes from `mainsdk` today. Whether the driver kit becomes its own package or part of `@mainsdk/testing` is still open.

## Run another implementation against the golden vectors

The golden test vectors are plain data, so a port to another language can prove it behaves the same:

```bash
npx mainsdk conformance export --out vectors/
npx mainsdk conformance run --impl "./my-port --vectors" --only config --json
```

`export` writes the vectors, and with `--check` it fails when the committed copy is stale. `run` feeds them to the command you name and exits 1 when a vector fails.

## Next

- [Testing kit](https://mainsdk.dev/docs/interfaces/testing)
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)
- [The check](https://mainsdk.dev/docs/concepts/the-check)
