Skip to content

Testing patterns

Put your queries behind small functions that take the client as an argument. Unit tests pass a stub; integration tests pass a real client pointed at a test project.

src/posts.ts
import type { VaifClient } from "@vaifstudio/client";
export type Post = { id: string; title: string; published: boolean };
export async function listPublished(vaif: VaifClient): Promise<Post[]> {
const { data } = await vaif.from<Post>("posts").eq("published", true).orderBy("title").list();
return data;
}
export async function createDraft(vaif: VaifClient, title: string): Promise<Post> {
const { data } = await vaif.from<Post>("posts").create({ title, published: false });
return data;
}
tests/posts.test.ts
import { describe, it, expect, vi } from "vitest";
import type { VaifClient } from "@vaifstudio/client";
import { listPublished } from "../src/posts";
function stubClient(rows: unknown[]): VaifClient {
const chain = {
eq: vi.fn(() => chain),
orderBy: vi.fn(() => chain),
list: vi.fn(async () => ({ data: rows, count: rows.length })),
};
return { from: vi.fn(() => chain) } as unknown as VaifClient;
}
describe("listPublished", () => {
it("returns the published posts", async () => {
const vaif = stubClient([{ id: "1", title: "Hello", published: true }]);
await expect(listPublished(vaif)).resolves.toHaveLength(1);
expect(vaif.from).toHaveBeenCalledWith("posts");
});
});

Run them against a separate project used only for tests, with its own keys. The secret key used here bypasses row rules, so keep it in your CI secrets and never in the repository:

tests/posts.integration.test.ts
import { describe, it, expect, afterAll } from "vitest";
import { createVaifClient } from "@vaifstudio/client";
import { createDraft } from "../src/posts";
const vaif = createVaifClient({
baseUrl: "https://api.vaif.studio",
projectId: process.env.VAIF_TEST_PROJECT_ID!,
apiKey: process.env.VAIF_TEST_SECRET_KEY!,
});
const created: string[] = [];
describe("posts (integration)", () => {
it("creates and reads back a draft", async () => {
const draft = await createDraft(vaif, "Integration test");
created.push(draft.id);
const { data } = await vaif.from("posts").get(draft.id);
expect(data.title).toBe("Integration test");
});
afterAll(async () => {
for (const id of created) await vaif.from("posts").delete(id);
});
});

Row rules apply to the user’s access token, so test them as a user: sign in as two test users, write a row as one, and check that the other cannot read it. This example assumes a notes table with an owner rule (see Row rules).

import { createVaifClient } from "@vaifstudio/client";
async function clientFor(email: string, password: string) {
let token = "";
const vaif = createVaifClient({
baseUrl: "https://api.vaif.studio",
projectId: process.env.VAIF_TEST_PROJECT_ID,
apiKey: process.env.VAIF_TEST_PUBLISHABLE_KEY,
accessToken: () => token,
});
const result = await vaif.auth.login(email, password);
if ("accessToken" in result) token = result.accessToken;
return vaif;
}
const alice = await clientFor("alice@test.example", process.env.ALICE_PASSWORD!);
const bob = await clientFor("bob@test.example", process.env.BOB_PASSWORD!);
const { data: note } = await alice.from("notes").create({ body: "private" });
const { data: seenByBob } = await bob.from("notes").eq("id", String(note.id)).list();
if (seenByBob.length !== 0) throw new Error("row rule is not applied");
.github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
- if: github.ref == 'refs/heads/main'
run: npm run test:integration
env:
VAIF_TEST_PROJECT_ID: ${{ secrets.VAIF_TEST_PROJECT_ID }}
VAIF_TEST_SECRET_KEY: ${{ secrets.VAIF_TEST_SECRET_KEY }}
  • Never point tests at your production project.
  • Clean up what a test creates.
  • Keep keys in your CI provider’s secrets, never in the repository.