Checkstack provides a comprehensive set of testing utilities specifically designed for backend packages. These utilities enable fast, deterministic unit tests by providing sophisticated mocks for core services like databases, loggers, queues, and RPC contexts.
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.
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" });
});
getStats() returns current queue stateCreates 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);
});
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]);
});
});
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();
});
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";
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();
});
When TypeScript complains about mock overrides:
// Use 'as any' for type assertion when overriding
(mockDb.select as any) = mock(() => customBehavior);
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() };
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])),
})),
}));
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:
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: