Skip to content

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.

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/backend in unit tests. Always use the dedicated test utility packages.

  • 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

Add the test utilities to your devDependencies:

{
"devDependencies": {
"@checkstack/test-utils-backend": "workspace:*",
"@checkstack/backend-api": "workspace:*"
}
}
// Import core mocking utilities
import {
createMockDb,
createMockLogger,
createMockFetch,
createMockQueueManager,
} from "@checkstack/test-utils-backend";
// Import RPC context mocking
import { createMockRpcContext } from "@checkstack/backend-api/test-utils";
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...");
});
});

Creates a mock Drizzle database instance with support for the most common query patterns.

Source: core/test-utils-backend/src/mock-db.ts

The mock supports chainable Drizzle queries:

import { createMockDb } from "@checkstack/test-utils-backend";
const mockDb = createMockDb();
// SELECT queries
await 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 queries
await mockDb.insert(usersTable).values({ name: "Alice" });
await mockDb.insert(usersTable).values({ name: "Bob" }).onConflictDoUpdate({ ... });
await mockDb.insert(usersTable).values({ name: "Charlie" }).returning();
// UPDATE queries
await mockDb.update(usersTable).set({ name: "Updated" }).where(eq(usersTable.id, "123"));
await mockDb.update(usersTable).set({ name: "Updated" }).returning();
// DELETE queries
await mockDb.delete(usersTable).where(eq(usersTable.id, "123"));
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();
});

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);
});

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 module
mock.module("./db", () => createMockDbModule());
// Now imports from './db' will use the mock
import { db } from "./db"; // This is mocked

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

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");
});

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" });
});

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" });
import { mock } from "bun:test";
import { createMockLoggerModule } from "@checkstack/test-utils-backend";
mock.module("./logger", () => createMockLoggerModule());
import { rootLogger } from "./logger"; // This is mocked

Creates a mock Fetch service for testing HTTP requests and inter-plugin communication.

Source: core/test-utils-backend/src/mock-fetch.ts

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();
});

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");
});
const fetch = createMockFetch();
// Generic fetch
await fetch.fetch("https://example.com");
// Plugin-scoped requests
const 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");
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);
});

Creates a mock QueueManager that produces simple in-memory mock queues for testing.

Source: core/test-utils-backend/src/mock-queue-factory.ts

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();
});
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" });
});
  • 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

Creates a complete mock RPC context with all dependencies pre-configured.

Source: core/backend-api/src/test-utils.ts

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();
});

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
}

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();
});
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);
});

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);
});
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");
});
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" });
});

❌ Bad: Recreating mocks locally

// DON'T DO THIS
const 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();

❌ Bad: Importing from production packages

// DON'T DO THIS - triggers production initialization
import { EventBus } from "@checkstack/backend";

✅ Good: Import only what you need

// Import test utilities from dedicated packages
import { 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 test
mock.module("./db", () => createMockDbModule());
// Now this import will use the mock
import { myServiceThatImportsDb } from "./my-service";

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" }])),
}));

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);

”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:

// ✅ Correct
const mockDb = createMockDb();
// ❌ Incorrect
const 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 mocking
mock.module("./db", () => createMockDbModule());
// Option 2: Direct imports (no side effects)
import { MyClass } from "./my-class"; // Not from index

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;

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 first
await 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);

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: