The recommended approach for mocking NestJS services in Bun tests.

Why Raw Mocks?

  1. No dependencies - Uses built-in bun:test
  2. Simple - No library quirks to work around
  3. Sufficient - Tests catch errors at runtime anyway
  4. 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 value
fn.mockReturnValue("new value");
fn.mockResolvedValue({ data: "async" });
fn.mockRejectedValue(new Error("fail"));
// Custom implementation
fn.mockImplementation((arg) => {
if (arg === "special") return "special result";
return "default";
});
// Assertions
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledWith("arg1", "arg2");
// Reset
fn.mockClear(); // Clear call history
fn.mockReset(); // Clear history + implementation

Helper 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,
});
// Usage
mockParser.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

  1. Use mock() from bun:test for all mock functions
  2. Use any type for mock objects - type safety isn’t worth the overhead
  3. Create helper functions for complex test data (these CAN be typed)
  4. Use mockImplementation for argument-dependent behavior
  5. Keep it simple - no external mocking libraries needed