Backend Utilities
The @checkstack/test-utils-backend package bundles mocks and helpers for writing fast, side-effect-free unit tests against backend plugin code. This page walks through every utility and the patterns we use them in.
Overview
Section titled “Overview”Philosophy: Side-Effect-Free Testing
Section titled “Philosophy: Side-Effect-Free Testing”The test utilities in Checkstack follow a critical principle: prevent side-effect poisoning during test execution.
Side-effect poisoning occurs when importing a module triggers production code that requires a full production environment (e.g., database connections, environment variable validation, service initialization). This makes tests fragile, slow, and environment-dependent.
To prevent this, Checkstack externalizes all testing utilities into dedicated packages:
@checkstack/test-utils-backend: Core mocks for backend services (DB, Logger, Fetch, Queues)@checkstack/backend-api/test-utils: Mocks for API-level structures (RpcContext)
CRITICAL: Never import from main entry points like
@checkstack/backendin unit tests. Always use the dedicated test utility packages.
Why Use These Utilities?
Section titled “Why Use These Utilities?”- Consistent behavior: Standardized mocks across the entire codebase
- Chainable interfaces: Mocks support the same fluent API as production services
- Zero configuration: Works out of the box with sensible defaults
- Type-safe: Full TypeScript support with proper type inference
- Maintained centrally: Updates propagate to all tests automatically
Installation and Setup
Section titled “Installation and Setup”Adding to Your Package
Section titled “Adding to Your Package”Add the test utilities to your devDependencies:
{ "devDependencies": { "@checkstack/test-utils-backend": "workspace:*", "@checkstack/backend-api": "workspace:*" }}Import Patterns
Section titled “Import Patterns”// Import core mocking utilitiesimport { createMockDb, createMockLogger, createMockFetch, createMockQueueManager,} from "@checkstack/test-utils-backend";
// Import RPC context mockingimport { createMockRpcContext } from "@checkstack/backend-api/test-utils";Basic Test Structure
Section titled “Basic Test Structure”import { describe, test, expect, mock } from "bun:test";import { createMockDb, createMockLogger } from "@checkstack/test-utils-backend";import { MyService } from "./my-service";
describe("MyService", () => { test("should process data correctly", async () => { const mockDb = createMockDb(); const mockLogger = createMockLogger(); const service = new MyService(mockDb, mockLogger);
await service.processData({ foo: "bar" });
expect(mockDb.insert).toHaveBeenCalled(); expect(mockLogger.info).toHaveBeenCalledWith("Processing data..."); });});Core Utilities Reference
Section titled “Core Utilities Reference”createMockDb
Section titled “createMockDb”Creates a mock Drizzle database instance with support for the most common query patterns.
Source: core/test-utils-backend/src/mock-db.ts
Supported Query Patterns
Section titled “Supported Query Patterns”The mock supports chainable Drizzle queries:
import { createMockDb } from "@checkstack/test-utils-backend";
const mockDb = createMockDb();
// SELECT queriesawait mockDb.select().from(usersTable);await mockDb.select().from(usersTable).where(eq(usersTable.id, "123"));await mockDb.select().from(usersTable).where(eq(usersTable.id, "123")).limit(10);await mockDb.select().from(usersTable).innerJoin(rolesTable).where(condition);await mockDb.select().from(usersTable).where(condition).orderBy(usersTable.name).limit(5);
// INSERT queriesawait mockDb.insert(usersTable).values({ name: "Alice" });await mockDb.insert(usersTable).values({ name: "Bob" }).onConflictDoUpdate({ ... });await mockDb.insert(usersTable).values({ name: "Charlie" }).returning();
// UPDATE queriesawait mockDb.update(usersTable).set({ name: "Updated" }).where(eq(usersTable.id, "123"));await mockDb.update(usersTable).set({ name: "Updated" }).returning();
// DELETE queriesawait mockDb.delete(usersTable).where(eq(usersTable.id, "123"));Basic Usage
Section titled “Basic Usage”import { createMockDb } from "@checkstack/test-utils-backend";
test("should fetch user from database", async () => { const mockDb = createMockDb(); const service = new UserService(mockDb);
await service.getUserById("user-1");
expect(mockDb.select).toHaveBeenCalled();});Customizing Return Values
Section titled “Customizing Return Values”For complex queries, you can override the mock to return specific data:
import { createMockDb } from "@checkstack/test-utils-backend";
test("should return specific user data", async () => { const mockDb = createMockDb(); const mockUserData = [{ id: "user-1", name: "Alice", role: "admin" }];
// Override the select chain to return mock data (mockDb.select as any) = mock(() => ({ from: mock(() => ({ innerJoin: mock(() => ({ where: mock(() => Promise.resolve(mockUserData)), })), })), }));
const service = new UserService(mockDb); const result = await service.getUserWithRole("user-1");
expect(result).toEqual(mockUserData);});Module Mocking
Section titled “Module Mocking”For tests that import the database module directly, use createMockDbModule:
import { mock } from "bun:test";import { createMockDbModule } from "@checkstack/test-utils-backend";
// Mock the entire database modulemock.module("./db", () => createMockDbModule());
// Now imports from './db' will use the mockimport { db } from "./db"; // This is mockedcreateMockLogger
Section titled “createMockLogger”Creates a mock logger instance with support for all standard logging levels and child logger creation.
Source: core/test-utils-backend/src/mock-logger.ts
Basic Usage
Section titled “Basic Usage”import { createMockLogger } from "@checkstack/test-utils-backend";
test("should log service initialization", async () => { const mockLogger = createMockLogger(); const service = new MyService(mockLogger);
await service.initialize();
expect(mockLogger.info).toHaveBeenCalledWith("Service initialized");});Child Logger Support
Section titled “Child Logger Support”The mock logger’s child() method returns another mock logger:
test("should use child logger for component", () => { const mockLogger = createMockLogger(); const service = new MyService(mockLogger);
service.processWithComponent("data-processor");
expect(mockLogger.child).toHaveBeenCalledWith({ component: "data-processor" });});Available Methods
Section titled “Available Methods”All standard logging levels are supported:
const logger = createMockLogger();
logger.info("Information message");logger.debug("Debug message");logger.warn("Warning message");logger.error("Error message");const childLogger = logger.child({ context: "my-component" });Module Mocking
Section titled “Module Mocking”import { mock } from "bun:test";import { createMockLoggerModule } from "@checkstack/test-utils-backend";
mock.module("./logger", () => createMockLoggerModule());
import { rootLogger } from "./logger"; // This is mockedcreateMockFetch
Section titled “createMockFetch”Creates a mock Fetch service for testing HTTP requests and inter-plugin communication.
Source: core/test-utils-backend/src/mock-fetch.ts
Basic Usage
Section titled “Basic Usage”import { createMockFetch } from "@checkstack/test-utils-backend";
test("should make HTTP request", async () => { const mockFetch = createMockFetch(); const service = new ExternalApiService(mockFetch);
await service.fetchData();
expect(mockFetch.fetch).toHaveBeenCalled();});Plugin-Scoped Requests
Section titled “Plugin-Scoped Requests”The forPlugin() method provides shortcuts for common HTTP methods:
test("should call catalog plugin API", async () => { const mockFetch = createMockFetch(); const service = new IntegrationService(mockFetch);
await service.getCatalogEntities();
expect(mockFetch.forPlugin).toHaveBeenCalledWith("catalog-backend");});Available Methods
Section titled “Available Methods”const fetch = createMockFetch();
// Generic fetchawait fetch.fetch("https://example.com");
// Plugin-scoped requestsconst catalogApi = fetch.forPlugin("catalog-backend");await catalogApi.get("/entities");await catalogApi.post("/entities", { body: data });await catalogApi.put("/entities/123", { body: data });await catalogApi.patch("/entities/123", { body: data });await catalogApi.delete("/entities/123");Customizing Responses
Section titled “Customizing Responses”test("should handle API response", async () => { const mockFetch = createMockFetch(); const mockData = { entities: [{ id: "1", name: "Service A" }] };
// Override to return specific data (mockFetch.forPlugin as any) = mock(() => ({ get: mock(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockData) }) ), }));
const service = new IntegrationService(mockFetch); const result = await service.getCatalogEntities();
expect(result).toEqual(mockData);});createMockQueueManager
Section titled “createMockQueueManager”Creates a mock QueueManager that produces simple in-memory mock queues for testing.
Source: core/test-utils-backend/src/mock-queue-factory.ts
Basic Usage
Section titled “Basic Usage”import { createMockQueueManager } from "@checkstack/test-utils-backend";
test("should enqueue job", async () => { const mockQueueManager = createMockQueueManager(); const queue = mockQueueManager.getQueue("my-channel");
const jobId = await queue.enqueue({ task: "process-data" });
expect(jobId).toBeDefined();});Testing Queue Consumers
Section titled “Testing Queue Consumers”test("should process queued jobs", async () => { const mockQueueManager = createMockQueueManager(); const queue = mockQueueManager.getQueue("tasks"); const processedJobs: any[] = [];
// Register consumer await queue.consume(async (job) => { processedJobs.push(job.data); }, { consumerGroup: "test-group" });
// Enqueue job (consumer is triggered immediately in mock) await queue.enqueue({ task: "send-email" });
expect(processedJobs).toHaveLength(1); expect(processedJobs[0]).toEqual({ task: "send-email" });});Key Features
Section titled “Key Features”- Immediate execution: Jobs are processed synchronously when enqueued (testing-friendly)
- Consumer groups: Supports multiple consumer groups
- Error handling: Catches errors like production queues
- Statistics:
getStats()returns current queue state
createMockRpcContext
Section titled “createMockRpcContext”Creates a complete mock RPC context with all dependencies pre-configured.
Source: core/backend-api/src/test-utils.ts
Basic Usage
Section titled “Basic Usage”import { createMockRpcContext } from "@checkstack/backend-api/test-utils";
test("should handle RPC request", async () => { const ctx = createMockRpcContext(); const result = await myRpcHandler({ ctx, input: { id: "123" } });
expect(ctx.db.select).toHaveBeenCalled(); expect(result).toBeDefined();});Context Properties
Section titled “Context Properties”The mock context includes:
interface RpcContext { db: MockDb; // Chainable database mock logger: MockLogger; // Logger with child() support fetch: MockFetch; // HTTP/inter-plugin requests auth: MockAuth; // Authentication methods healthCheckRegistry: MockRegistry; queuePluginRegistry: MockRegistry; queueManager: MockQueueManager; // Queue management mock user?: User; // Optional authenticated user}Overriding Properties
Section titled “Overriding Properties”You can override specific properties for your test:
test("should handle authenticated request", async () => { const ctx = createMockRpcContext({ user: { id: "user-123", email: "test@example.com", role: "admin" }, });
const result = await protectedHandler({ ctx, input: {} });
expect(ctx.user?.role).toBe("admin"); expect(result).toBeDefined();});Testing Router Handlers
Section titled “Testing Router Handlers”import { createMockRpcContext } from "@checkstack/backend-api/test-utils";import { myRouter } from "./router";
test("should handle getUser RPC call", async () => { const ctx = createMockRpcContext(); const mockUser = { id: "user-1", name: "Alice" };
(ctx.db.select as any) = mock(() => ({ from: mock(() => ({ where: mock(() => Promise.resolve([mockUser])), })), }));
const caller = myRouter.createCaller(ctx); const result = await caller.getUser({ id: "user-1" });
expect(result).toEqual(mockUser);});Common Usage Patterns
Section titled “Common Usage Patterns”Pattern 1: Testing Services with Database Dependencies
Section titled “Pattern 1: Testing Services with Database Dependencies”import { describe, test, expect } from "bun:test";import { createMockDb, createMockLogger } from "@checkstack/test-utils-backend";import { UserService } from "./user-service";
describe("UserService", () => { test("should create new user", async () => { const mockDb = createMockDb(); const mockLogger = createMockLogger(); const service = new UserService(mockDb, mockLogger);
await service.createUser({ name: "Alice", email: "alice@example.com" });
expect(mockDb.insert).toHaveBeenCalled(); expect(mockLogger.info).toHaveBeenCalledWith("User created"); });
test("should fetch user by ID", async () => { const mockDb = createMockDb(); const mockLogger = createMockLogger(); const mockUserData = [{ id: "1", name: "Alice" }];
(mockDb.select as any) = mock(() => ({ from: mock(() => ({ where: mock(() => Promise.resolve(mockUserData)), })), }));
const service = new UserService(mockDb, mockLogger); const result = await service.getUserById("1");
expect(result).toEqual(mockUserData[0]); });});Pattern 2: Testing Routers with Full Context
Section titled “Pattern 2: Testing Routers with Full Context”import { test, expect } from "bun:test";import { createMockRpcContext } from "@checkstack/backend-api/test-utils";import { createUserRouter } from "./router";
test("should handle createUser RPC call", async () => { const ctx = createMockRpcContext({ user: { id: "admin-1", role: "admin" }, });
const router = createUserRouter(); const caller = router.createCaller(ctx);
await caller.createUser({ name: "Bob", email: "bob@example.com" });
expect(ctx.db.insert).toHaveBeenCalled(); expect(ctx.logger.info).toHaveBeenCalled();});Pattern 3: Testing Inter-Plugin Communication
Section titled “Pattern 3: Testing Inter-Plugin Communication”import { test, expect } from "bun:test";import { createMockFetch } from "@checkstack/test-utils-backend";import { CatalogIntegration } from "./catalog-integration";
test("should fetch entities from catalog plugin", async () => { const mockFetch = createMockFetch(); const mockEntities = [{ id: "1", name: "Service A" }];
(mockFetch.forPlugin as any) = mock(() => ({ get: mock(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ entities: mockEntities }) }) ), }));
const integration = new CatalogIntegration(mockFetch); const result = await integration.getEntities();
expect(mockFetch.forPlugin).toHaveBeenCalledWith("catalog-backend"); expect(result.entities).toEqual(mockEntities);});Pattern 4: Testing Queue Consumers
Section titled “Pattern 4: Testing Queue Consumers”import { test, expect } from "bun:test";import { createMockQueueManager, createMockLogger } from "@checkstack/test-utils-backend";import { EmailWorker } from "./email-worker";
test("should process email jobs", async () => { const mockQueueManager = createMockQueueManager(); const mockLogger = createMockLogger(); const worker = new EmailWorker(mockQueueManager, mockLogger);
const queue = mockQueueManager.getQueue("emails"); const sentEmails: any[] = [];
await queue.consume(async (job) => { sentEmails.push(job.data); }, { consumerGroup: "test-group" });
await queue.enqueue({ to: "user@example.com", subject: "Test" });
expect(sentEmails).toHaveLength(1); expect(sentEmails[0].to).toBe("user@example.com");});Pattern 5: Testing with Child Loggers
Section titled “Pattern 5: Testing with Child Loggers”import { test, expect } from "bun:test";import { createMockLogger } from "@checkstack/test-utils-backend";import { MultiStepProcessor } from "./processor";
test("should use child loggers for each step", async () => { const mockLogger = createMockLogger(); const processor = new MultiStepProcessor(mockLogger);
await processor.run();
expect(mockLogger.child).toHaveBeenCalledWith({ step: "validate" }); expect(mockLogger.child).toHaveBeenCalledWith({ step: "transform" }); expect(mockLogger.child).toHaveBeenCalledWith({ step: "save" });});Best Practices
Section titled “Best Practices”1. Always Use Centralized Utilities
Section titled “1. Always Use Centralized Utilities”❌ Bad: Recreating mocks locally
// DON'T DO THISconst mockDb = { select: mock(() => ({ from: mock(() => Promise.resolve([])) })), insert: mock(() => ({ values: mock(() => Promise.resolve()) })),};✅ Good: Import from test utilities
import { createMockDb } from "@checkstack/test-utils-backend";
const mockDb = createMockDb();2. Avoid Importing from Main Entry Points
Section titled “2. Avoid Importing from Main Entry Points”❌ Bad: Importing from production packages
// DON'T DO THIS - triggers production initializationimport { EventBus } from "@checkstack/backend";✅ Good: Import only what you need
// Import test utilities from dedicated packagesimport { createMockQueueManager } from "@checkstack/test-utils-backend";
// Import classes directly (no side effects)import { EventBus } from "@checkstack/backend/event-bus";3. Use Module Mocking for Integration Tests
Section titled “3. Use Module Mocking for Integration Tests”When testing code that imports modules directly:
import { mock } from "bun:test";import { createMockDbModule } from "@checkstack/test-utils-backend";
// Mock the module before importing the code under testmock.module("./db", () => createMockDbModule());
// Now this import will use the mockimport { myServiceThatImportsDb } from "./my-service";4. Customize Mocks Only When Necessary
Section titled “4. Customize Mocks Only When Necessary”Start with the default mock and override only when you need specific behavior:
const mockDb = createMockDb();
// Override only the specific method you need(mockDb.select as any) = mock(() => ({ from: mock(() => Promise.resolve([{ id: "1", name: "Test" }])),}));5. Test Asynchronous Operations Properly
Section titled “5. Test Asynchronous Operations Properly”Always await async operations in tests:
test("should process async operation", async () => { const service = new MyService(mockDb);
// ✅ Await the operation await service.processAsync();
expect(mockDb.insert).toHaveBeenCalled();});6. Use Type Assertions for Complex Overrides
Section titled “6. Use Type Assertions for Complex Overrides”When TypeScript complains about mock overrides:
// Use 'as any' for type assertion when overriding(mockDb.select as any) = mock(() => customBehavior);Troubleshooting
Section titled “Troubleshooting””Cannot read property ‘from’ of undefined”
Section titled “”Cannot read property ‘from’ of undefined””Problem: The mock chain is broken.
Solution: Ensure you’re using createMockDb() and not manually creating partial mocks:
// ✅ Correctconst mockDb = createMockDb();
// ❌ Incorrectconst mockDb = { select: mock() };“Module evaluation triggered database connection”
Section titled ““Module evaluation triggered database connection””Problem: Side-effect poisoning from importing production code.
Solution: Use module mocking or import directly from sub-paths:
// Option 1: Module mockingmock.module("./db", () => createMockDbModule());
// Option 2: Direct imports (no side effects)import { MyClass } from "./my-class"; // Not from index“Mock not being called as expected”
Section titled ““Mock not being called as expected””Problem: The mock was overridden incorrectly.
Solution: Check that you’re overriding the right method in the chain:
// Verify which method you need to override(mockDb.select as any) = mock(() => ({ from: mock(() => ({ where: mock(() => Promise.resolve([expectedData])), })),}));“Type error when using createMockRpcContext”
Section titled ““Type error when using createMockRpcContext””Problem: TypeScript can’t infer the correct types.
Solution: Use type assertions or provide explicit overrides:
import { RpcContext } from "@checkstack/backend-api";
const ctx = createMockRpcContext({ user: { id: "1", email: "test@example.com" } as any,}) as RpcContext;“Queue consumer not being called”
Section titled ““Queue consumer not being called””Problem: The mock queue processes jobs immediately and synchronously.
Solution: Remember that createMockQueueManager executes consumers immediately when jobs are enqueued:
const results: any[] = [];
// Register consumer firstawait queue.consume(async (job) => { results.push(job.data);}, { consumerGroup: "test" });
// Then enqueue (triggers consumer immediately)await queue.enqueue({ data: "test" });
// Results available immediately (no need to wait)expect(results).toHaveLength(1);Summary
Section titled “Summary”The Checkstack backend test utilities provide:
- Comprehensive mocking for all core services (DB, Logger, Fetch, Queue, RPC)
- Chainable interfaces that match production behavior
- Zero configuration with sensible defaults
- Side-effect-free execution to keep tests fast and reliable
- Centralized maintenance for consistency across the codebase
Always use these utilities from their dedicated packages (@checkstack/test-utils-backend, @checkstack/backend-api/test-utils) to ensure clean, maintainable, and reliable tests.
For reference implementations, see: