Raw Bun Mocks Guide
The recommended approach for mocking NestJS services in Bun tests.
Why Raw Mocks?
- No dependencies - Uses built-in
bun:test - Simple - No library quirks to work around
- Sufficient - Tests catch errors at runtime anyway
- Fast - No proxy overhead
Basic Pattern
import { beforeEach, describe, expect, it, mock } from "bun:test";import { SyncService } from "./sync.service.js";
describe("SyncService", () => { let service: SyncService; let mockManifest: any; let mockParser: any; let mockGraph: any;
beforeEach(() => { // Create mock objects with mock functions mockManifest = { load: mock(() => Promise.resolve({ version: "1.0", documents: {} })), save: mock(() => Promise.resolve()), detectChange: mock(() => "new"), getTrackedPaths: mock(() => []), };
mockParser = { discoverDocuments: mock(() => Promise.resolve([])), parseDocument: mock(() => Promise.resolve(createDoc())), };
mockGraph = { upsertNode: mock(() => Promise.resolve()), upsertRelationship: mock(() => Promise.resolve()), deleteNode: mock(() => Promise.resolve()), query: mock(() => Promise.resolve({ resultSet: [] })), };
service = new SyncService(mockManifest, mockParser, mockGraph); });
it("syncs new documents", async () => { mockParser.discoverDocuments.mockImplementation(() => Promise.resolve(["docs/new.md"]) );
const result = await service.sync();
expect(result.added).toBe(1); expect(mockGraph.upsertNode).toHaveBeenCalled(); });});Mock Function API
Bun’s mock() provides all the methods you need:
const fn = mock(() => "default");
// Change return valuefn.mockReturnValue("new value");fn.mockResolvedValue({ data: "async" });fn.mockRejectedValue(new Error("fail"));
// Custom implementationfn.mockImplementation((arg) => { if (arg === "special") return "special result"; return "default";});
// Assertionsexpect(fn).toHaveBeenCalled();expect(fn).toHaveBeenCalledTimes(2);expect(fn).toHaveBeenCalledWith("arg1", "arg2");
// Resetfn.mockClear(); // Clear call historyfn.mockReset(); // Clear history + implementationHelper Functions for Test Data
Create typed helpers for complex test data:
import type { ParsedDocument } from "./document-parser.service.js";
const createDoc = (overrides: Partial<ParsedDocument> = {}): ParsedDocument => ({ path: "docs/test.md", title: "Test Document", content: "# Test", contentHash: "abc123", frontmatterHash: "def456", entities: [], relationships: [], tags: [], ...overrides,});
// UsagemockParser.parseDocument.mockImplementation(() => Promise.resolve(createDoc({ path: "docs/custom.md" })));Argument-Dependent Behavior
Use mockImplementation for conditional returns:
mockManifest.detectChange.mockImplementation((path: string) => { if (path === "docs/new.md") return "new"; if (path === "docs/updated.md") return "updated"; return "unchanged";});Error Scenarios
mockParser.parseDocument.mockImplementation((path: string) => { if (path === "docs/bad.md") { return Promise.reject(new Error("Parse error")); } return Promise.resolve(createDoc({ path }));});Complete Example
import { beforeEach, describe, expect, it, mock } from "bun:test";import type { ParsedDocument } from "./document-parser.service.js";import { SyncService } from "./sync.service.js";
const createDoc = (overrides: Partial<ParsedDocument> = {}): ParsedDocument => ({ path: "docs/test.md", title: "Test Document", content: "# Test", contentHash: "abc123", frontmatterHash: "def456", entities: [], relationships: [], tags: [], ...overrides,});
describe("SyncService", () => { let service: SyncService; let mockManifest: any; let mockParser: any; let mockGraph: any; let mockCascade: any; let mockPathResolver: any;
beforeEach(() => { mockManifest = { load: mock(() => Promise.resolve({ version: "1.0", lastSync: new Date().toISOString(), documents: {}, })), save: mock(() => Promise.resolve()), detectChange: mock(() => "new"), getTrackedPaths: mock(() => []), };
mockParser = { discoverDocuments: mock(() => Promise.resolve([])), parseDocument: mock(() => Promise.resolve(createDoc())), };
mockGraph = { query: mock(() => Promise.resolve({ resultSet: [], stats: undefined })), upsertNode: mock(() => Promise.resolve()), upsertRelationship: mock(() => Promise.resolve()), deleteNode: mock(() => Promise.resolve()), deleteDocumentRelationships: mock(() => Promise.resolve()), };
mockCascade = { analyzeDocumentChange: mock(() => Promise.resolve([])), };
mockPathResolver = { getDocsPath: mock(() => "/home/user/project/docs"), resolveDocPaths: mock((paths: string[]) => paths), isUnderDocs: mock(() => true), };
service = new SyncService( mockManifest, mockParser, mockGraph, mockCascade, mockPathResolver, ); });
describe("detectChanges", () => { it("returns new documents not in manifest", async () => { mockParser.discoverDocuments.mockImplementation(() => Promise.resolve(["docs/new.md"]) ); mockParser.parseDocument.mockImplementation(() => Promise.resolve(createDoc({ path: "docs/new.md" })) );
const changes = await service.detectChanges();
expect(changes).toHaveLength(1); expect(changes[0].path).toBe("docs/new.md"); expect(changes[0].changeType).toBe("new"); });
it("returns deleted documents no longer on disk", async () => { mockManifest.getTrackedPaths.mockImplementation(() => ["docs/deleted.md"]);
const changes = await service.detectChanges();
expect(changes).toHaveLength(1); expect(changes[0].path).toBe("docs/deleted.md"); expect(changes[0].changeType).toBe("deleted"); }); });
describe("sync", () => { it("returns correct counts for mixed changes", async () => { mockParser.discoverDocuments.mockImplementation(() => Promise.resolve(["docs/new.md", "docs/updated.md", "docs/unchanged.md"]) ); mockParser.parseDocument.mockImplementation((path: string) => Promise.resolve(createDoc({ path })) ); mockManifest.detectChange.mockImplementation((path: string) => { if (path === "docs/new.md") return "new"; if (path === "docs/updated.md") return "updated"; return "unchanged"; }); mockManifest.getTrackedPaths.mockImplementation(() => [ "docs/updated.md", "docs/unchanged.md", "docs/deleted.md", ]);
const result = await service.sync();
expect(result.added).toBe(1); expect(result.updated).toBe(1); expect(result.unchanged).toBe(1); expect(result.deleted).toBe(1); });
it("handles parse errors gracefully", async () => { mockParser.discoverDocuments.mockImplementation(() => Promise.resolve(["docs/good.md", "docs/bad.md"]) ); mockParser.parseDocument.mockImplementation((path: string) => { if (path === "docs/bad.md") { return Promise.reject(new Error("Parse error")); } return Promise.resolve(createDoc({ path })); });
const result = await service.sync();
expect(result.errors.length).toBeGreaterThanOrEqual(1); expect(result.errors.some((e) => e.path === "docs/bad.md")).toBe(true); }); });});Summary
- Use
mock()frombun:testfor all mock functions - Use
anytype for mock objects - type safety isn’t worth the overhead - Create helper functions for complex test data (these CAN be typed)
- Use
mockImplementationfor argument-dependent behavior - Keep it simple - no external mocking libraries needed