Testing
GraphDB’s synchronous reads and simple factory API make it easy to test. This page shows common testing patterns using Bun’s built-in test runner.
Test helper: fresh database per test
Shared mutable state between tests leads to flaky results. Create a new database and collection for each test.
import { describe, it, expect, beforeEach } from "bun:test";import { GraphDB, type Collection, type Doc } from "@graphdb/core";
interface Task { title: string; priority: number; done: boolean;}
function createTestCollection() { const db = GraphDB(); return db.createCollection<Task>("tasks", { indexes: ["priority", "done"], });}
describe("tasks collection", () => { let tasks: Collection<Task>;
beforeEach(() => { tasks = createTestCollection(); });
// tests go here...});Every test starts with an empty collection, so ordering and isolation are guaranteed.
Testing CRUD operations
Create and read
describe("create and read", () => { let tasks: Collection<Task>;
beforeEach(() => { tasks = createTestCollection(); });
it("should create a document and return its id", async () => { const id = await tasks.create({ title: "Write tests", priority: 1, done: false, });
expect(id).toBeString(); expect(id.length).toBeGreaterThan(0); });
it("should read back the created document", async () => { const id = await tasks.create({ title: "Write tests", priority: 1, done: false, });
const doc = tasks.read(id);
expect(doc).not.toBeNull(); expect(doc!.title).toBe("Write tests"); expect(doc!.priority).toBe(1); expect(doc!.done).toBe(false); expect(doc!._id).toBe(id); expect(doc!.createdAt).toBeNumber(); expect(doc!.updatedAt).toBeNumber(); });
it("should return null for a nonexistent id", () => { const doc = tasks.read("does-not-exist"); expect(doc).toBeNull(); });});Update
describe("update", () => { let tasks: Collection<Task>;
beforeEach(() => { tasks = createTestCollection(); });
it("should update specific fields and bump updatedAt", async () => { const id = await tasks.create({ title: "Original", priority: 3, done: false, });
const before = tasks.read(id)!; // Small delay so updatedAt differs await new Promise((r) => setTimeout(r, 10));
const updated = await tasks.update(id, { done: true, priority: 1 });
expect(updated.done).toBe(true); expect(updated.priority).toBe(1); expect(updated.title).toBe("Original"); // unchanged expect(updated.updatedAt).toBeGreaterThanOrEqual(before.updatedAt); });});Remove
describe("remove", () => { let tasks: Collection<Task>;
beforeEach(() => { tasks = createTestCollection(); });
it("should remove a document and return acknowledgment", async () => { const id = await tasks.create({ title: "To remove", priority: 2, done: false, });
const result = await tasks.remove(id);
expect(result.removedId).toBe(id); expect(result.acknowledge).toBe(true); expect(tasks.read(id)).toBeNull(); });
it("should reflect removal in count", async () => { await tasks.create({ title: "A", priority: 1, done: false }); const idB = await tasks.create({ title: "B", priority: 2, done: false });
expect(tasks.count()).toBe(2);
await tasks.remove(idB);
expect(tasks.count()).toBe(1); });});Testing queries
describe("queries", () => { let tasks: Collection<Task>;
beforeEach(async () => { tasks = createTestCollection();
await tasks.create({ title: "Low priority task", priority: 3, done: false }); await tasks.create({ title: "High priority task", priority: 1, done: false }); await tasks.create({ title: "Medium done task", priority: 2, done: true }); await tasks.create({ title: "High done task", priority: 1, done: true }); });
it("should filter by primitive equality", () => { const results = tasks.query({ done: true }); expect(results).toHaveLength(2); results.forEach((doc) => expect(doc.done).toBe(true)); });
it("should filter with comparison operators", () => { const highPriority = tasks.query({ priority: { lte: 1 } }); expect(highPriority).toHaveLength(2);
const lowPriority = tasks.query({ priority: { gt: 2 } }); expect(lowPriority).toHaveLength(1); expect(lowPriority[0]!.title).toBe("Low priority task"); });
it("should filter with string operators", () => { const results = tasks.query({ title: { startsWith: "High" } }); expect(results).toHaveLength(2); });
it("should filter with the in operator", () => { const results = tasks.query({ priority: { in: [1, 3] } }); expect(results).toHaveLength(3); });
it("should combine multiple where fields with AND logic", () => { const results = tasks.query({ priority: 1, done: false }); expect(results).toHaveLength(1); expect(results[0]!.title).toBe("High priority task"); });
it("should support orderBy, skip, and limit", () => { const page = tasks.query( {}, { orderBy: { priority: "ASC" }, skip: 1, limit: 2 }, );
expect(page).toHaveLength(2); // After sorting by priority ASC (1,1,2,3) and skipping 1, we get index 1 and 2 });
it("should return an empty array when nothing matches", () => { const results = tasks.query({ priority: { gt: 100 } }); expect(results).toEqual([]); });});Testing findOne, count, and exists
describe("findOne, count, exists", () => { let tasks: Collection<Task>;
beforeEach(async () => { tasks = createTestCollection(); await tasks.create({ title: "Alpha", priority: 1, done: false }); await tasks.create({ title: "Beta", priority: 2, done: true }); });
it("findOne should return the first match", () => { const result = tasks.findOne({ done: true }); expect(result).not.toBeNull(); expect(result!.title).toBe("Beta"); });
it("findOne should return null when nothing matches", () => { const result = tasks.findOne({ priority: 99 }); expect(result).toBeNull(); });
it("count should return total or filtered count", () => { expect(tasks.count()).toBe(2); expect(tasks.count({ done: false })).toBe(1); });
it("exists should check by id", async () => { const id = await tasks.create({ title: "C", priority: 3, done: false }); expect(tasks.exists(id)).toBe(true); expect(tasks.exists("nonexistent")).toBe(false); });});Testing event listeners
describe("event listeners", () => { let tasks: Collection<Task>;
beforeEach(() => { tasks = createTestCollection(); });
it("should fire create event with the new document", async () => { const events: Doc<Task>[] = []; tasks.on("create", ({ doc }) => events.push(doc));
await tasks.create({ title: "Test", priority: 1, done: false });
expect(events).toHaveLength(1); expect(events[0]!.title).toBe("Test"); });
it("should fire update event with before, after, and patch", async () => { const id = await tasks.create({ title: "Before", priority: 1, done: false });
let captured: { before: Doc<Task>; after: Doc<Task>; patch: Partial<Task> } | null = null; tasks.on("update", (event) => { captured = event; });
await tasks.update(id, { title: "After", done: true });
expect(captured).not.toBeNull(); expect(captured!.before.title).toBe("Before"); expect(captured!.after.title).toBe("After"); expect(captured!.patch).toEqual({ title: "After", done: true }); });
it("should fire remove event with the removed document", async () => { const id = await tasks.create({ title: "Gone", priority: 1, done: false });
const removed: Doc<Task>[] = []; tasks.on("remove", ({ doc }) => removed.push(doc));
await tasks.remove(id);
expect(removed).toHaveLength(1); expect(removed[0]!.title).toBe("Gone"); });
it("should fire populate event with count", async () => { let count = 0; tasks.on("populate", (event) => { count = event.count; });
await tasks.populate([ { title: "A", priority: 1, done: false }, { title: "B", priority: 2, done: false }, { title: "C", priority: 3, done: true }, ]);
expect(count).toBe(3); });
it("should stop firing after unsubscribe", async () => { const events: Doc<Task>[] = []; const unsub = tasks.on("create", ({ doc }) => events.push(doc));
await tasks.create({ title: "First", priority: 1, done: false }); unsub(); await tasks.create({ title: "Second", priority: 2, done: false });
expect(events).toHaveLength(1); expect(events[0]!.title).toBe("First"); });
it("should fire per-document listener on update", async () => { const id = await tasks.create({ title: "Watch me", priority: 1, done: false });
const snapshots: Doc<Task>[] = []; const cancel = tasks.listen(id, (doc) => snapshots.push(doc));
await tasks.update(id, { priority: 5 }); await tasks.update(id, { done: true });
cancel(); await tasks.update(id, { title: "Ignored" });
expect(snapshots).toHaveLength(2); expect(snapshots[0]!.priority).toBe(5); expect(snapshots[1]!.done).toBe(true); });});Testing syncers
Successful sync
describe("syncers - success", () => { it("should call the create syncer with the document", async () => { const calls: Doc<Task>[] = [];
const db = GraphDB(); const tasks = db.createCollection<Task>("tasks", { syncers: { create: async (doc) => { calls.push(doc); return true; }, }, });
const id = await tasks.create({ title: "Synced", priority: 1, done: false });
// Allow the syncer to resolve await new Promise((r) => setTimeout(r, 50));
expect(calls).toHaveLength(1); expect(calls[0]!._id).toBe(id); expect(calls[0]!.title).toBe("Synced");
// Document should still exist since syncer returned true expect(tasks.read(id)).not.toBeNull(); });});Failed sync with revert
describe("syncers - failure and revert", () => { it("should revert a create when syncer returns false", async () => { const db = GraphDB(); const tasks = db.createCollection<Task>("tasks", { syncers: { create: async () => false, // reject every create }, });
const errors: Array<{ op: string; docId?: string }> = []; tasks.on("syncError", ({ op, docId }) => { errors.push({ op, docId }); });
const id = await tasks.create({ title: "Will revert", priority: 1, done: false });
// The document exists optimistically right after create expect(tasks.read(id)).not.toBeNull();
// Wait for the syncer to run and revert await new Promise((r) => setTimeout(r, 50));
// After revert, the document should be gone expect(tasks.read(id)).toBeNull(); expect(errors).toHaveLength(1); expect(errors[0]!.op).toBe("create"); });
it("should revert an update when syncer returns false", async () => { const db = GraphDB(); const tasks = db.createCollection<Task>("tasks", { syncers: { update: async () => false, }, });
const id = await tasks.create({ title: "Original", priority: 1, done: false });
await tasks.update(id, { title: "Changed" });
// Wait for syncer to reject and revert await new Promise((r) => setTimeout(r, 50));
const doc = tasks.read(id); expect(doc).not.toBeNull(); expect(doc!.title).toBe("Original"); // reverted });
it("should revert a remove when syncer returns false", async () => { const db = GraphDB(); const tasks = db.createCollection<Task>("tasks", { syncers: { remove: async () => false, }, });
const id = await tasks.create({ title: "Persistent", priority: 1, done: false });
await tasks.remove(id);
// Wait for syncer to reject and revert await new Promise((r) => setTimeout(r, 50));
// Document should be restored const doc = tasks.read(id); expect(doc).not.toBeNull(); expect(doc!.title).toBe("Persistent"); });});Syncer that throws
describe("syncers - exceptions", () => { it("should emit syncError when syncer throws", async () => { const db = GraphDB(); const tasks = db.createCollection<Task>("tasks", { syncers: { create: async () => { throw new Error("Network failure"); }, }, });
const errors: Array<{ op: string; error: unknown }> = []; tasks.on("syncError", ({ op, error }) => { errors.push({ op, error }); });
await tasks.create({ title: "Doomed", priority: 1, done: false });
// Wait for the syncer to throw and the error event to fire await new Promise((r) => setTimeout(r, 50));
expect(errors).toHaveLength(1); expect(errors[0]!.op).toBe("create"); expect(errors[0]!.error).toBeInstanceOf(Error); });});Testing bulk operations
describe("bulk operations", () => { let tasks: Collection<Task>;
beforeEach(() => { tasks = createTestCollection(); });
it("populate should insert multiple documents", async () => { await tasks.populate([ { title: "A", priority: 1, done: false }, { title: "B", priority: 2, done: true }, { title: "C", priority: 3, done: false }, ]);
expect(tasks.count()).toBe(3); });
it("updateMany should patch all matching documents", async () => { await tasks.populate([ { title: "A", priority: 1, done: false }, { title: "B", priority: 2, done: false }, { title: "C", priority: 3, done: true }, ]);
await tasks.updateMany({ done: false }, { done: true });
const allDone = tasks.query({ done: true }); expect(allDone).toHaveLength(3); });
it("removeMany should delete all matching documents", async () => { await tasks.populate([ { title: "A", priority: 1, done: false }, { title: "B", priority: 2, done: true }, { title: "C", priority: 3, done: true }, ]);
await tasks.removeMany({ done: true });
expect(tasks.count()).toBe(1); expect(tasks.findOne({ title: "A" })).not.toBeNull(); });
it("clear should remove all documents", async () => { await tasks.populate([ { title: "A", priority: 1, done: false }, { title: "B", priority: 2, done: false }, ]);
await tasks.clear();
expect(tasks.count()).toBe(0); });});