Use when writing tests for Bknd applications, setting up test infrastructure, creating unit/integration tests, or testing API endpoints. Covers in-memory database setup, test helpers, mocking, and test patterns.
Write and run tests for Bknd applications using Bun Test or Vitest with in-memory databases for isolation.
Bun has a built-in test runner:
# Run all tests
bun test
# Run specific file
bun test tests/posts.test.ts
# Watch mode
bun test --watch
# Install
bun add -D vitest
# Configure vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
},
});
# Run
npx vitest
Use in-memory SQLite for fast, isolated tests.
Create tests/helper.ts:
import { App, createApp as baseCreateApp } from "bknd";
import { em, entity, text, number, boolean } from "bknd";
import Database from "libsql";
// Schema for tests
export const testSchema = em({
posts: entity("posts", {
title: text().required(),
content: text(),
published: boolean(),
}),
comments: entity("comments", {
body: text().required(),
author: text(),
}),
}, (fn, s) => {
fn.relation(s.comments).manyToOne(s.posts);
});
// Create isolated test app with in-memory DB
export async function createTestApp(options?: {
seed?: (app: App) => Promise<void>;
}) {
const db = new Database(":memory:");
const app = new App({
connection: { database: db },
schema: testSchema,
});
await app.build();
if (options?.seed) {
await options.seed(app);
}
return {
app,
cleanup: () => {
db.close();
},
};
}
// Create test API client
export async function createTestClient(app: App) {
const baseUrl = "http://localhost:0"; // Placeholder
return {
data: app.modules.data,
auth: app.modules.auth,
};
}
For Bun's native SQLite:
import { bunSqlite } from "bknd/adapter/bun";
import { Database } from "bun:sqlite";
export function createTestConnection() {
const db = new Database(":memory:");
return bunSqlite({ database: db });
}
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { createTestApp } from "./helper";
describe("Posts", () => {
let app: Awaited<ReturnType<typeof createTestApp>>;
beforeEach(async () => {
app = await createTestApp();
});
afterEach(() => {
app.cleanup();
});
test("creates a post", async () => {
const result = await app.app.em
.mutator("posts")
.insertOne({ title: "Test Post", content: "Hello" });
expect(result.id).toBeDefined();
expect(result.title).toBe("Test Post");
});
test("reads posts", async () => {
// Seed data
await app.app.em.mutator("posts").insertOne({ title: "Post 1" });
await app.app.em.mutator("posts").insertOne({ title: "Post 2" });
const posts = await app.app.em.repo("posts").findMany();
expect(posts).toHaveLength(2);
});
test("updates a post", async () => {
const created = await app.app.em
.mutator("posts")
.insertOne({ title: "Original" });
const updated = await app.app.em
.mutator("posts")
.updateOne(created.id, { title: "Updated" });
expect(updated.title).toBe("Updated");
});
test("deletes a post", async () => {
const created = await app.app.em
.mutator("posts")
.insertOne({ title: "To Delete" });
await app.app.em.mutator("posts").deleteOne(created.id);
const found = await app.app.em.repo("posts").findOne(created.id);
expect(found).toBeNull();
});
});
describe("Comments", () => {
let app: Awaited<ReturnType<typeof createTestApp>>;
beforeEach(async () => {
app = await createTestApp();
});
afterEach(() => app.cleanup());
test("creates comment with relation", async () => {
const post = await app.app.em
.mutator("posts")
.insertOne({ title: "Parent Post" });
const comment = await app.app.em
.mutator("comments")
.insertOne({
body: "Great post!",
posts_id: post.id,
});
expect(comment.posts_id).toBe(post.id);
});
test("loads comments with post", async () => {
const post = await app.app.em
.mutator("posts")
.insertOne({ title: "Post" });
await app.app.em.mutator("comments").insertOne({
body: "Comment 1",
posts_id: post.id,
});
const comments = await app.app.em.repo("comments").findMany({
with: { posts: true },
});
expect(comments[0].posts).toBeDefined();
expect(comments[0].posts.title).toBe("Post");
});
});
Test the full HTTP stack:
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { serve } from "bknd/adapter/bun";
describe("API Integration", () => {
let server: ReturnType<typeof Bun.serve>;
const port = 3999;
const baseUrl = `http://localhost:${port}`;
beforeAll(async () => {
server = Bun.serve({
port,
fetch: (await serve({
connection: { url: ":memory:" },
schema: testSchema,
})).fetch,
});
});
afterAll(() => {
server.stop();
});
test("GET /api/data/posts returns 200", async () => {
const res = await fetch(`${baseUrl}/api/data/posts`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toEqual({ data: [] });
});
test("POST /api/data/posts creates record", async () => {
const res = await fetch(`${baseUrl}/api/data/posts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "API Test" }),
});
expect(res.status).toBe(201);
const { data } = await res.json();
expect(data.title).toBe("API Test");
});
});
import { Api } from "bknd/client";
describe("SDK Integration", () => {
let api: Api;
let server: ReturnType<typeof Bun.serve>;
beforeAll(async () => {
// Start test server
server = await startTestServer();
api = new Api({ host: "http://localhost:3999" });
});
afterAll(() => server.stop());
test("creates and reads via SDK", async () => {
const created = await api.data.createOne("posts", {
title: "SDK Test",
});
expect(created.ok).toBe(true);
const read = await api.data.readOne("posts", created.data.id);
expect(read.data.title).toBe("SDK Test");
});
});
describe("Authentication", () => {
let app: Awaited<ReturnType<typeof createTestApp>>;
beforeEach(async () => {
app = await createTestApp({
auth: {
enabled: true,
strategies: {
password: {
hashing: "plain", // Only for tests!
},
},
},
});
});
afterEach(() => app.cleanup());
test("registers a user", async () => {
const auth = app.app.modules.auth;
const result = await auth.register({
email: "test@example.com",
password: "password123",
});
expect(result.user).toBeDefined();
expect(result.user.email).toBe("test@example.com");
});
test("login with correct password", async () => {
const auth = app.app.modules.auth;
// Register first
await auth.register({
email: "test@example.com",
password: "password123",
});
// Then login
const result = await auth.login({
email: "test@example.com",
password: "password123",
});
expect(result.token).toBeDefined();
});
test("login with wrong password fails", async () => {
const auth = app.app.modules.auth;
await auth.register({
email: "test@example.com",
password: "correct",
});
await expect(
auth.login({
email: "test@example.com",
password: "wrong",
})
).rejects.toThrow();
});
});
import { mock, jest } from "bun:test";
describe("External API calls", () => {
let originalFetch: typeof fetch;
beforeAll(() => {
originalFetch = global.fetch;
// @ts-ignore
global.fetch = jest.fn(() =>
Promise.resolve(
new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
);
});
afterAll(() => {
global.fetch = originalFetch;
});
test("FetchTask uses mocked fetch", async () => {
const task = new FetchTask("test", {
url: "https://api.example.com/data",
method: "GET",
});
const result = await task.run();
expect(result.success).toBe(true);
expect(global.fetch).toHaveBeenCalled();
});
});
describe("Email sending", () => {
test("uses mock email driver", async () => {
const sentEmails: any[] = [];
const app = await createTestApp({
drivers: {
email: {
send: async (to, subject, body) => {
sentEmails.push({ to, subject, body });
return { id: "mock-id" };
},
},
},
});
// Trigger something that sends email
await app.app.drivers.email.send(
"user@example.com",
"Test",
"Body"
);
expect(sentEmails).toHaveLength(1);
expect(sentEmails[0].to).toBe("user@example.com");
app.cleanup();
});
});
Create reusable factories for test data:
// tests/factories.ts
let counter = 0;
export function createPostData(overrides = {}) {
counter++;
return {
title: `Test Post ${counter}`,
content: `Content for post ${counter}`,
published: false,
...overrides,
};
}
export function createUserData(overrides = {}) {
counter++;
return {
email: `user${counter}@test.com`,
password: "password123",
...overrides,
};
}
// Usage in tests
test("creates multiple posts", async () => {
const posts = await Promise.all([
app.em.mutator("posts").insertOne(createPostData()),
app.em.mutator("posts").insertOne(createPostData({ published: true })),
app.em.mutator("posts").insertOne(createPostData()),
]);
expect(posts).toHaveLength(3);
});
import { Flow, FetchTask, Condition } from "bknd/flows";
describe("Flows", () => {
test("executes flow with tasks", async () => {
const task1 = new FetchTask("fetch", {
url: "https://example.com/api",
method: "GET",
});
const flow = new Flow("testFlow", [task1]);
const execution = await flow.start({ input: "value" });
expect(execution.hasErrors()).toBe(false);
expect(execution.getResponse()).toBeDefined();
});
test("handles task errors", async () => {
const failingTask = new FetchTask("fail", {
url: "https://invalid-url-that-fails.test",
method: "GET",
});
const flow = new Flow("failFlow", [failingTask]);
const execution = await flow.start({});
expect(execution.hasErrors()).toBe(true);
expect(execution.getErrors()).toHaveLength(1);
});
});
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun install
- run: bun test
# .husky/pre-commit
#!/bin/sh
bun test --bail
my-bknd-app/
├── src/
│ └── ...
├── tests/
│ ├── helper.ts # Test utilities
│ ├── factories.ts # Data factories
│ ├── unit/
│ │ ├── posts.test.ts
│ │ └── auth.test.ts
│ └── integration/
│ ├── api.test.ts
│ └── flows.test.ts
├── bknd.config.ts
└── package.json
Problem: Tests share state, causing flaky tests.
Solution: Create fresh in-memory DB per test:
beforeEach(async () => {
app = await createTestApp(); // New DB each time
});
afterEach(() => {
app.cleanup(); // Close connection
});
Problem: Tests hang or leak resources.
Solution: Always await cleanup:
afterEach(async () => {
await app.cleanup();
});
afterAll(async () => {
await server.stop();
});
Problem: Test passes before async operation completes.
Solution: Always await async operations:
// WRONG
test("fails silently", () => {
expect(api.data.readMany("posts")).resolves.toBeDefined();
});
// CORRECT
test("properly awaited", async () => {
const result = await api.data.readMany("posts");
expect(result).toBeDefined();
});
Problem: Tests modify real data.
Solution: Always use :memory: or test-specific file:
// SAFE
connection: { url: ":memory:" }
// ALSO SAFE
connection: { url: "file:test-${Date.now()}.db" }
// DANGEROUS - never in tests
connection: { url: process.env.DB_URL }
DO:
DON'T:
plain password hashing outside testsnpx skills add cameronapak/bknd-testing下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer