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 Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
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; this guide walks through the common tasks.
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 install --save-dev @mainsdk/testingpnpm add -D @mainsdk/testingbun add -d @mainsdk/testing2. 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.
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
node --test tests/checkout.test.tspnpm exec node --test tests/checkout.test.tsThe 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:
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
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.
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.
fallback:
email: [resend, ses]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.
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 };
},
});npx mainsdk driver test ./relaymail --interface email@1.4email@1.4 conformance: 86 passed, 0 failed · capability profile: attachments=no · idempotency=sdkA 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:
npx mainsdk conformance export --out vectors/
npx mainsdk conformance run --impl "./my-port --vectors" --only config --jsonexport 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
Was this page helpful?