Claude-skill-registry bknd-testing
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/bknd-testing" ~/.claude/skills/majiayu000-claude-skill-registry-bknd-testing && rm -rf "$T"
skills/data/bknd-testing/SKILL.mdTesting Bknd Applications
Write and run tests for Bknd applications using Bun Test or Vitest with in-memory databases for isolation.
Prerequisites
- Bknd project set up locally
- Test runner installed (Bun or Vitest)
- Understanding of async/await patterns
When to Use UI Mode
- Manual integration testing via admin panel
- Verifying data after test runs
- Quick smoke testing
When to Use Code Mode
- Automated unit tests
- Integration tests
- CI/CD pipelines
- Regression testing
Test Runner Setup
Bun (Recommended)
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
Vitest
# Install bun add -D vitest # Configure vitest.config.ts import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, }, }); # Run npx vitest
In-Memory Database Setup
Use in-memory SQLite for fast, isolated tests.
Test Helper Module
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, }; }
Bun-Specific Helper
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 }); }
Unit Testing Patterns
Testing Entity Operations
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(); }); });
Testing Relationships
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"); }); });
Integration Testing
HTTP API Testing
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"); }); });
Testing with SDK Client
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"); }); });
Testing Authentication
Auth Flow Testing
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(); }); });
Mocking Patterns
Mocking Fetch
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(); }); });
Mocking Drivers
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(); }); });
Test Data Factories
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); });
Testing Flows
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); }); });
CI/CD Configuration
GitHub Actions
# .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
Pre-commit Hook
# .husky/pre-commit #!/bin/sh bun test --bail
Project Structure
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
Common Pitfalls
Database Not Isolated
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 });
Async Cleanup Issues
Problem: Tests hang or leak resources.
Solution: Always await cleanup:
afterEach(async () => { await app.cleanup(); }); afterAll(async () => { await server.stop(); });
Missing await on Assertions
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(); });
Testing Against Production DB
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 }
DOs and DON'Ts
DO:
- Use in-memory databases for speed and isolation
- Clean up resources in afterEach/afterAll
- Create test helpers and factories
- Test both success and error paths
- Use meaningful test descriptions
- Keep tests independent of each other
DON'T:
- Share database state between tests
- Use production credentials in tests
- Skip await on async operations
- Write tests that depend on execution order
- Use
password hashing outside testsplain - Commit test database files
Related Skills
- bknd-local-setup - Development environment setup
- bknd-debugging - Troubleshooting test failures
- bknd-seed-data - Creating test data patterns
- bknd-crud-create - Understanding data operations
- bknd-setup-auth - Auth configuration for tests