Testing Utilities
Database testing utilities for unit and integration tests in the TanStack Start Prisma SaaS Kit.
The @kit/database package includes testing utilities for writing unit and integration tests with real database interactions. No mocking required.
This guide is part of the Database Configuration documentation.
Overview
The testing utilities provide:
- PGlite - In-memory PostgreSQL database for fast, isolated tests
- Factory functions - Generate test data with sensible defaults
- Auth context utilities - Test multi-tenant authorization logic
Importing
The testing utilities are exported from dedicated subpaths of @kit/database:
// Factory functions and PGlite helpersimport { createTestUser, createTestOrganization } from '@kit/database/testing';// PGlite-specific utilitiesimport { createTestDatabase } from '@kit/database/testing/pglite';In-Memory Database (PGlite)
PGlite is PostgreSQL compiled to WebAssembly, running fully in-memory for fast, isolated testing without Docker or external services.
createTestDatabase()
Creates an in-memory PostgreSQL database with every migration from src/prisma/migrations applied, and returns a real Prisma client bound to it:
import { createTestDatabase, type TestDatabase,} from '@kit/database/testing/pglite';describe('User Service', () => { let testDb: TestDatabase; beforeAll(async () => { testDb = await createTestDatabase(); }); afterEach(async () => { await testDb.cleanup(); // Clear all data between tests }); afterAll(async () => { await testDb.close(); // Close connection }); it('should create a user', async () => { const user = await testDb.db.user.create({ data: { id: 'user_123', email: 'test@example.com', name: 'Test User', emailVerified: true, createdAt: new Date(), updatedAt: new Date(), }, }); expect(user.email).toBe('test@example.com'); });});TestDatabase Interface
interface TestDatabase { db: PrismaClient; // Prisma client bound to PGlite client: PGlite; // Raw PGlite client cleanup: () => Promise<void>; // Truncate all tables close: () => Promise<void>; // Disconnect and close}Tables Cleared by cleanup()
The cleanup() method truncates these tables with CASCADE for test isolation:
rateLimitmemberinvitationorganizationRolesubscriptionorganizationsessionaccounttwoFactorverificationuser
Never test against a real database
createTestDatabase() runs entirely in-memory via PGlite, so it is always isolated. If you write custom integration helpers that connect to a real PostgreSQL instance, always point them at a dedicated test database — cleanup() truncates all tables and will destroy data.
Factory Functions
Generate test data with sensible defaults. Factories return plain objects — pass them to create().
createTestUser()
import { createTestUser } from '@kit/database/testing';// With defaultsconst user = createTestUser();// { id: 'abc123...', email: 'test-abc123...@makerkit.dev', name: 'Test User abc1', ... }// With overridesconst admin = createTestUser({ email: 'admin@example.com', name: 'Admin User',});Default Values:
| Field | Default |
|---|---|
id | Random 32-character hex string |
email | test-{id}@makerkit.dev |
emailVerified | true |
name | Test User {first 4 chars of id} |
image | null |
createdAt | Current date |
updatedAt | Current date |
createTestOrganization()
import { createTestOrganization } from '@kit/database/testing';const org = createTestOrganization({ name: 'Acme Corp', slug: 'acme-corp',});Default Values:
| Field | Default |
|---|---|
id | Random 32-character hex string |
name | Test Organization {first 4 chars of id} |
slug | test-org-{id} |
logo | null |
metadata | "{}" |
createdAt | Current date |
createTestMember()
import { createTestUser, createTestOrganization, createTestMember,} from '@kit/database/testing';const user = createTestUser();const org = createTestOrganization();const member = createTestMember({ userId: user.id, // Required (defaults to empty string) organizationId: org.id, // Required (defaults to empty string) role: 'owner',});Default Values:
| Field | Default |
|---|---|
id | Random 32-character hex string |
userId | Empty string (must override) |
organizationId | Empty string (must override) |
role | 'member' |
createdAt | Current date |
createTestSubscription()
import { createTestOrganization, createTestSubscription,} from '@kit/database/testing';const org = createTestOrganization();const subscription = createTestSubscription({ referenceId: org.id, plan: 'enterprise-annual', seats: 10,});Default Values:
| Field | Default |
|---|---|
id | Random 32-character hex string |
plan | 'pro-monthly' |
referenceId | Empty string (should override) |
customer_id | cus_test_{id} |
subscription_id | sub_test_{id} |
status | 'active' |
periodStart | Current date |
periodEnd | 30 days from now |
cancelAtPeriodEnd | false |
seats | 1 |
Auth Context Utilities
Utilities for testing multi-tenant authorization. They return a plain context object carrying the current user, organization, and role — use its fields in your queries.
createAuthContext()
Creates a base authorization context for an authenticated user:
import { createAuthContext, db } from '@kit/database';const ctx = createAuthContext( 'user_123', // userId null, // organizationId (optional) null, // role (optional));// Use the context fields in your queriesconst sessions = await db.session.findMany({ where: { userId: ctx.userId },});createOrgAuthContext()
Creates an organization-scoped authorization context (organizationId and role are required):
import { createOrgAuthContext, db } from '@kit/database';const ctx = createOrgAuthContext( 'user_123', // userId 'org_456', // organizationId (required) 'admin', // role (required));// Filter by organization using the contextconst members = await db.member.findMany({ where: { organizationId: ctx.organizationId },});AuthorizationError
Custom error class for authorization failures:
import { AuthorizationError } from '@kit/database';throw AuthorizationError.notAuthenticated();// Error: "Not authenticated"throw AuthorizationError.noOrganization();// Error: "No active organization"throw AuthorizationError.notAuthorized('project');// Error: "Not authorized to access project"Complete Example
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';import { createTestDatabase } from '@kit/database/testing/pglite';import { createTestUser, createTestOrganization, createTestMember, createTestSubscription,} from '@kit/database/testing';import { createOrgAuthContext } from '@kit/database';describe('Organization Billing', () => { let testDb: Awaited<ReturnType<typeof createTestDatabase>>; beforeAll(async () => { testDb = await createTestDatabase(); }); afterEach(async () => { await testDb.cleanup(); }); afterAll(async () => { await testDb.close(); }); it('should create organization with subscription', async () => { // Create test data using factories const testUser = createTestUser(); const testOrg = createTestOrganization(); const testMember = createTestMember({ userId: testUser.id, organizationId: testOrg.id, role: 'owner', }); // Insert with the Prisma client await testDb.db.user.create({ data: testUser }); await testDb.db.organization.create({ data: testOrg }); await testDb.db.member.create({ data: testMember }); await testDb.db.subscription.create({ data: { id: crypto.randomUUID(), plan: 'pro-monthly', referenceId: testOrg.id, status: 'active', }, }); // Create auth context for queries const ctx = createOrgAuthContext(testUser.id, testOrg.id, 'owner'); // Query with the context's organizationId const result = await testDb.db.subscription.findFirst({ where: { referenceId: ctx.organizationId }, }); expect(result?.plan).toBe('pro-monthly'); expect(result?.status).toBe('active'); });});Common Mistakes to Avoid
Forgetting to call cleanup() in afterEach: Tests that share data can produce flaky results. Always truncate tables between tests for isolation.
Using the global db instead of testDb.db: The global db connects to your development database. Always use testDb.db in tests.
Not awaiting async operations: Factory functions return plain objects, but database operations return promises. Always await create(), findMany(), etc.
Vitest Configuration
The database package runs tests with Vitest (pnpm --filter @kit/database test:unit). A typical config:
vitest.config.ts
import { defineConfig } from 'vitest/config';export default defineConfig({ test: { globals: true, environment: 'node', include: ['**/*.test.ts'], },});Frequently Asked Questions
What is PGlite and why is it used for testing?
Why does cleanup() use TRUNCATE instead of DELETE?
How do I test multi-tenant authorization?
Do factories write to the database?
Previous: Rate Limit Service