Database Testing Guide
Database Testing Guide
Practical guide for testing Prisma-based code, storage adapters, Neon API services, and the D1 cache sync layer in the bloqr-backend.
Table of Contents
- Test Architecture
- Running Tests
- PrismaClient Mock Patterns
- Testing HyperdriveStorageAdapter
- Testing D1StorageAdapter
- Testing D1 Cache Sync
- Testing Neon API Service
- Testing Clerk Webhooks (Prisma)
- Shared Test Fixtures
- Best Practices
Test Architecture
All tests run under Deno’s built-in test runner using Deno.test() and
@std/assert. Database tests use in-memory mocks — no real database
connections are needed.
flowchart TB
subgraph Runner["Deno Test Runner"]
direction TB
SRC["src/**/*.test.ts<br/>(storage, services)"]
WRK["worker/**/*.test.ts<br/>(middleware, handlers)"]
E2E["worker/*.e2e.test.ts<br/>(API + WebSocket)"]
end
subgraph Mocks["Mock Layer (in-memory)"]
MP["Mock PrismaClient<br/>(Map-backed stores)"]
MD1["Mock D1Database<br/>(prepare/bind/run)"]
MH["Mock Hyperdrive<br/>(connection string)"]
MF["Stub fetch()<br/>(Neon API)"]
end
subgraph Real["Real Targets (E2E only)"]
DB["Docker PostgreSQL<br/>or Neon Branch"]
WK["Cloudflare Worker<br/>(wrangler dev)"]
end
SRC --> MP
SRC --> MD1
SRC --> MH
SRC --> MF
WRK --> MP
WRK --> MH
E2E --> WK --> DB
Key Principles
| Principle | Why |
|---|---|
| No real database in unit tests | Tests must run without Docker, Neon, or network access |
| Map-backed stores | Each mock model uses a Map<string, Record> for in-memory CRUD |
| Capturing mocks | Record call arguments for assertion (capturedUpsertArgs) |
Stub globalThis.fetch | Intercept Neon API calls; restore originalFetch in cleanup |
| Deno permissions | Tests use --allow-read --allow-write --allow-net --allow-env |
Running Tests
| Command | Scope |
|---|---|
deno task test | All unit tests (src + worker) |
deno task test:src | Storage adapters, services, sync |
deno task test:worker | Middleware, handlers, auth |
deno task test:watch | Watch mode for src/ (re-runs on save) |
deno task test:watch:worker | Watch mode for worker/ |
deno task test:coverage | Coverage report for both src + worker |
deno task test:e2e | End-to-end tests (requires running Worker) |
deno task test:e2e:api | API endpoint E2E tests only |
deno task test:e2e:ws | WebSocket E2E tests only |
deno task test:contract | OpenAPI contract validation |
Running a Single Test File
deno test --allow-read --allow-write --allow-net --allow-env src/storage/HyperdriveStorageAdapter.test.tsRunning Tests Matching a Pattern
deno test --allow-read --allow-write --allow-net --allow-env --filter "upsert" src/PrismaClient Mock Patterns
The project uses two mock patterns depending on the test’s needs.
Pattern 1: Map-Backed Full Mock
For tests that need realistic CRUD behavior (storage adapters, sync):
function createMockPrismaClient(): any { // One Map per Prisma model const stores: Record<string, Map<string, Record<string, unknown>>> = { storageEntry: new Map(), filterCache: new Map(), compilationMetadata: new Map(), user: new Map(), apiKey: new Map(), };
function modelAccessor(name: string) { const store = stores[name]!; return { findUnique: async ({ where }: { where: { id?: string; key?: string } }) => { const key = where.id ?? where.key; return store.get(key!) ?? null; }, findMany: async () => Array.from(store.values()), create: async ({ data }: { data: Record<string, unknown> }) => { const id = (data.id as string) ?? crypto.randomUUID(); const record = { ...data, id }; store.set(id, record); return record; }, upsert: async ({ where, update, create }: { where: { id: string }; update: Record<string, unknown>; create: Record<string, unknown>; }) => { const existing = store.get(where.id); if (existing) { const merged = { ...existing, ...update }; store.set(where.id, merged); return merged; } store.set(create.id as string ?? where.id, create); return create; }, delete: async ({ where }: { where: { id: string } }) => { const record = store.get(where.id); store.delete(where.id); return record; }, deleteMany: async () => { const count = store.size; store.clear(); return { count }; }, count: async () => store.size, }; }
return { storageEntry: modelAccessor('storageEntry'), filterCache: modelAccessor('filterCache'), compilationMetadata: modelAccessor('compilationMetadata'), user: modelAccessor('user'), apiKey: modelAccessor('apiKey'), $disconnect: async () => {}, };}Pattern 2: Capturing Mock (for Assertions)
For tests that verify what was called, not the data flow:
interface MockPrismaOptions { upsertResult?: { id: string }; deleteManyResult?: { count: number }; upsertError?: Error;}
function createCapturingMockPrisma(opts: MockPrismaOptions = {}) { const mock = { capturedUpsertArgs: null as unknown, capturedDeleteArgs: null as unknown, user: { async upsert(args: unknown) { if (opts.upsertError) throw opts.upsertError; mock.capturedUpsertArgs = args; return opts.upsertResult ?? { id: 'uuid-mock-1' }; }, async deleteMany(args: unknown) { mock.capturedDeleteArgs = args; return opts.deleteManyResult ?? { count: 1 }; }, }, async $disconnect() { /* noop */ }, }; return mock;}
// Usage in a testDeno.test('webhook handler maps Clerk fields to Prisma upsert', async () => { const prisma = createCapturingMockPrisma(); await handleUserCreated(clerkPayload, prisma);
// Assert the exact args passed to prisma.user.upsert() const args = prisma.capturedUpsertArgs as any; assertEquals(args.where.clerkUserId, 'user_abc123'); assertEquals(args.create.email, 'test@example.com');});Testing HyperdriveStorageAdapter
The HyperdriveStorageAdapter wraps PrismaClient to implement the IStorageAdapter
interface for the L2 (PostgreSQL) storage tier.
Test file: src/storage/HyperdriveStorageAdapter.test.ts
Mock Setup
import { assertEquals, assertExists } from '@std/assert';import { HyperdriveStorageAdapter } from './HyperdriveStorageAdapter.ts';
// Mock the Hyperdrive bindingfunction createMockHyperdrive() { return { connectionString: 'postgresql://test:test@localhost:5432/test', host: 'localhost', port: 5432, user: 'test', password: 'test', database: 'test', };}
// Mock PrismaClient factory (returns the Map-backed mock)function createMockPrismaFactory() { const mockPrisma = createMockPrismaClient(); return { factory: (_connectionString: string) => mockPrisma, prisma: mockPrisma, };}
// Capturing logger for verifying log outputfunction createCapturingLogger() { const messages: Array<{ level: string; msg: string }> = []; return { messages, debug(msg: string) { messages.push({ level: 'debug', msg }); }, info(msg: string) { messages.push({ level: 'info', msg }); }, warn(msg: string) { messages.push({ level: 'warn', msg }); }, error(msg: string) { messages.push({ level: 'error', msg }); }, };}Example Test
Deno.test('HyperdriveStorageAdapter: set + get round-trip', async () => { const hyperdrive = createMockHyperdrive(); const { factory, prisma } = createMockPrismaFactory(); const logger = createCapturingLogger();
const adapter = new HyperdriveStorageAdapter(hyperdrive, factory, { enableLogging: true, }); adapter.setLogger(logger); await adapter.open();
// Write const ok = await adapter.set(['test', 'key1'], { value: 42 }, 60_000); assertEquals(ok, true);
// Read back const entry = await adapter.get(['test', 'key1']); assertExists(entry); assertEquals(entry.value, { value: 42 });
// Verify logging const setLog = logger.messages.find(m => m.msg.includes('set')); assertExists(setLog);
await adapter.close();});Testing D1StorageAdapter
The D1StorageAdapter wraps a Cloudflare D1 binding for the L1 (edge SQLite)
storage tier.
Test file: src/storage/D1StorageAdapter.test.ts
Mock D1 Binding
function createMockD1(): any { return { prepare: (_sql: string) => ({ bind: (..._values: unknown[]) => ({ first: async () => null, run: async () => ({ results: [], success: true, meta: { duration: 0, changes: 0, last_row_id: 0 }, }), all: async () => ({ results: [], success: true, meta: { duration: 0, changes: 0, last_row_id: 0 }, }), raw: async () => [], }), }), dump: async () => new ArrayBuffer(0), batch: async (stmts: unknown[]) => stmts.map(() => ({ results: [], success: true, meta: { duration: 0, changes: 0, last_row_id: 0 }, })), exec: async () => ({ count: 0, duration: 0 }), };}Example Test
Deno.test('D1StorageAdapter: lifecycle', async () => { const d1 = createMockD1(); const adapter = new D1StorageAdapter(d1);
assertEquals(adapter.isOpen(), false); await adapter.open(); assertEquals(adapter.isOpen(), true); await adapter.close(); assertEquals(adapter.isOpen(), false);});Testing D1 Cache Sync
The d1-cache-sync module synchronizes records from Neon (L2) to D1 (L1) using
write-through or lazy strategies.
Test file: src/storage/d1-cache-sync.test.ts
Mock D1 Prisma Delegate
type RecordStore = Map<string, Record<string, unknown>>;
function createMockDelegate(store: RecordStore = new Map()) { return { upsert: async ({ where, update, create }: { where: { id: string }; update: Record<string, unknown>; create: Record<string, unknown>; }) => { const existing = store.get(where.id); if (existing) { const merged = { ...existing, ...update }; store.set(where.id, merged); return merged; } store.set(create.id as string, create); return create; }, delete: async ({ where }: { where: { id: string } }) => { if (!store.has(where.id)) { const err = new Error('Record to delete does not exist.'); (err as any).code = 'P2025'; // Prisma "not found" error throw err; } const record = store.get(where.id); store.delete(where.id); return record; }, findUnique: async ({ where }: { where: { id: string } }) => { return store.get(where.id) ?? null; }, };}
// Full D1 Prisma mock with transaction supportfunction createMockD1Prisma( stores: Record<string, RecordStore> = {}, opts?: { transactionFail?: boolean },) { const getDelegate = (table: string) => { if (!stores[table]) stores[table] = new Map(); return createMockDelegate(stores[table]); };
return new Proxy({} as any, { get: (_, prop: string) => { if (prop === '$transaction') { return async (fn: (tx: any) => Promise<void>) => { if (opts?.transactionFail) throw new Error('Simulated transaction failure'); await fn(new Proxy({}, { get: (_, t: string) => getDelegate(t) })); }; } return getDelegate(prop); }, });}Example Test
import { syncRecord, invalidateRecord, isCacheStale } from './d1-cache-sync.ts';
Deno.test('syncRecord: writes data to D1 cache', async () => { const store: RecordStore = new Map(); const d1Prisma = createMockD1Prisma({ filterCache: store });
const result = await syncRecord( 'filterCache', 'fc-1', { source: 'easylist.txt', content: 'rule1', hash: 'abc' }, d1Prisma, );
assertEquals(result.success, true); assertEquals(result.table, 'filterCache'); assertEquals(store.has('fc-1'), true);});
Deno.test('invalidateRecord: removes from D1 cache', async () => { const store: RecordStore = new Map(); store.set('fc-1', { id: 'fc-1', source: 'easylist.txt' });
const d1Prisma = createMockD1Prisma({ filterCache: store }); const result = await invalidateRecord('filterCache', 'fc-1', d1Prisma);
assertEquals(result.success, true); assertEquals(store.has('fc-1'), false);});Testing Neon API Service
The NeonApiService wraps the Neon REST API v2. Tests stub globalThis.fetch
to intercept HTTP calls.
Test file: src/services/neonApiService.test.ts
Fetch Stubbing Pattern
// Save original fetch for cleanupconst originalFetch = globalThis.fetch;
// Simple stub — returns a fixed responsefunction stubFetch(status: number, body: unknown): void { (globalThis as any).fetch = ( _input: string | URL | Request, _init?: RequestInit, ): Promise<Response> => { return Promise.resolve( new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }), ); };}
// Spy stub — captures request details for assertionsfunction spyFetch(status: number, body: unknown) { const calls: Array<{ url: string; init: RequestInit | undefined }> = []; (globalThis as any).fetch = ( input: string | URL | Request, init?: RequestInit, ): Promise<Response> => { const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; calls.push({ url, init }); return Promise.resolve( new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }), ); }; return { calls };}Example Test
Deno.test({ name: 'NeonApiService: listBranches returns parsed branches', async fn() { const branches = [ { id: 'br-main-abc123', name: 'main', current_state: 'ready' }, { id: 'br-preview-def456', name: 'preview/pr-42', current_state: 'ready' }, ];
const spy = spyFetch(200, { branches });
const service = createNeonApiService({ apiKey: 'test-key' }); const result = await service.listBranches('twilight-river-73901472');
assertEquals(result.length, 2); assertEquals(result[0].name, 'main');
// Verify correct URL was called assertEquals(spy.calls.length, 1); assert(spy.calls[0].url.includes('/projects/twilight-river-73901472/branches'));
// Verify auth header assertEquals(spy.calls[0].init?.headers?.['Authorization'], 'Bearer test-key'); }, sanitizeOps: false, sanitizeResources: false, afterEach() { // Always restore original fetch globalThis.fetch = originalFetch; },});Fixtures
const FIXTURES = { project: { id: 'twilight-river-73901472', name: 'bloqr-backend', region_id: 'azure-eastus2', pg_version: 17, }, branch: { id: 'br-cool-night-abc123', name: 'main', project_id: 'twilight-river-73901472', current_state: 'ready', },};Testing Clerk Webhooks (Prisma)
Clerk webhook tests verify that incoming Clerk events (user.created,
user.updated, user.deleted) correctly map to Prisma user.upsert() /
user.deleteMany() calls.
Test file: worker/clerk-webhook.test.ts
Example Test
Deno.test('user.created webhook maps Clerk fields to Prisma upsert', async () => { const prisma = createCapturingMockPrisma({ upsertResult: { id: 'uuid-1' } });
const payload = { type: 'user.created', data: { id: 'user_clerk_abc', email_addresses: [{ email_address: 'alice@example.com', id: 'em_1' }], primary_email_address_id: 'em_1', first_name: 'Alice', last_name: 'Smith', image_url: 'https://img.clerk.com/abc', public_metadata: { tier: 'pro' }, }, };
await handleClerkUserWebhook(payload, prisma as any);
const args = prisma.capturedUpsertArgs as any; assertEquals(args.where.clerkUserId, 'user_clerk_abc'); assertEquals(args.create.email, 'alice@example.com'); assertEquals(args.create.firstName, 'Alice'); assertEquals(args.create.tier, 'pro');});Shared Test Fixtures
MockKVNamespace
A reusable in-memory KV store for testing KV-dependent code:
export class MockKVNamespace { private store: Map<string, { value: string | ArrayBuffer; expiration?: number }> = new Map();
async get(key: string, type?: 'text' | 'json' | 'arrayBuffer'): Promise<unknown> { const entry = this.store.get(key); if (!entry) return null; if (entry.expiration && Date.now() > entry.expiration) { this.store.delete(key); return null; } if (type === 'json' && typeof entry.value === 'string') { return JSON.parse(entry.value); } return entry.value; }
async put(key: string, value: string | ArrayBuffer, options?: { expirationTtl?: number }) { this.store.set(key, { value, expiration: options?.expirationTtl ? Date.now() + options.expirationTtl * 1000 : undefined, }); }
async delete(key: string) { this.store.delete(key); } async list() { return { keys: [...this.store.keys()].map(name => ({ name })) }; }}Best Practices
Do
- Always restore stubs — If you stub
globalThis.fetch, restoreoriginalFetchin anafterEachorfinallyblock - Use Map-backed stores — They give realistic CRUD behavior without a real database
- Use capturing mocks when you need to assert on exact call arguments
- Keep tests independent — Each
Deno.test()should create its own mocks; don’t share mutable state between tests - Use
sanitizeOps: falsefor tests with fire-and-forget operations (e.g., API keylast_used_atupdates)
Don’t
- Don’t connect to real databases in unit tests — use mocks
- Don’t import
PrismaClientdirectly in tests — mock the factory function - Don’t hardcode UUIDs — use
crypto.randomUUID()or deterministic test fixtures - Don’t skip error paths — test database errors, Prisma
P2025(not found), and network failures
Test File Naming
| Pattern | Location | Runner |
|---|---|---|
*.test.ts | src/ or worker/ | deno task test |
*.e2e.test.ts | worker/ | deno task test:e2e |
*.bench.ts | src/ | Excluded from test runs |
Further Reading
- Testing Overview — General testing strategy
- E2E Testing — End-to-end test setup and patterns
- Postman Testing — API testing with Postman collections
- Local Dev Guide — Setting up Docker PostgreSQL for E2E tests
Bloqr AI™ — The privacy you didn't know you needed.
© 2026 Bloqr AI™, a trademark of Bloqr Systems™. Created by Bloqr Systems™, founded by Jayson Knight.
Internet Hygiene (n.) — the ongoing practices that keep your digital life clean, private, and safe.
Our product repos live in the BloqrAI org, part of the Bloqr Systems GitHub Enterprise. Product repos are internal-visibility — enterprise membership is required to view them.