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 helpers
import { createTestUser, createTestOrganization } from '@kit/database/testing';
// PGlite-specific utilities
import { 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:

  • rateLimit
  • member
  • invitation
  • organizationRole
  • subscription
  • organization
  • session
  • account
  • twoFactor
  • verification
  • user

Factory Functions

Generate test data with sensible defaults. Factories return plain objects — pass them to create().

createTestUser()

import { createTestUser } from '@kit/database/testing';
// With defaults
const user = createTestUser();
// { id: 'abc123...', email: 'test-abc123...@makerkit.dev', name: 'Test User abc1', ... }
// With overrides
const admin = createTestUser({
email: 'admin@example.com',
name: 'Admin User',
});

Default Values:

FieldDefault
idRandom 32-character hex string
emailtest-{id}@makerkit.dev
emailVerifiedtrue
nameTest User {first 4 chars of id}
imagenull
createdAtCurrent date
updatedAtCurrent date

createTestOrganization()

import { createTestOrganization } from '@kit/database/testing';
const org = createTestOrganization({
name: 'Acme Corp',
slug: 'acme-corp',
});

Default Values:

FieldDefault
idRandom 32-character hex string
nameTest Organization {first 4 chars of id}
slugtest-org-{id}
logonull
metadata"{}"
createdAtCurrent 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:

FieldDefault
idRandom 32-character hex string
userIdEmpty string (must override)
organizationIdEmpty string (must override)
role'member'
createdAtCurrent 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:

FieldDefault
idRandom 32-character hex string
plan'pro-monthly'
referenceIdEmpty string (should override)
customer_idcus_test_{id}
subscription_idsub_test_{id}
status'active'
periodStartCurrent date
periodEnd30 days from now
cancelAtPeriodEndfalse
seats1

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 queries
const 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 context
const 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?
PGlite is PostgreSQL compiled to WebAssembly that runs in-memory. createTestDatabase() applies the real migrations from src/prisma/migrations and returns a Prisma client bound to it, so tests run against real PostgreSQL behavior without Docker or external services. Each test suite gets a fresh database.
Why does cleanup() use TRUNCATE instead of DELETE?
TRUNCATE is faster than DELETE for clearing tables because it doesn't scan rows. The CASCADE option handles foreign key constraints, keeping test isolation fast even with many tables.
How do I test multi-tenant authorization?
Use createOrgAuthContext() to build contexts for different users and organizations, then assert that queries filtered by ctx.organizationId only return that organization's data and reject access to others.
Do factories write to the database?
No. Factory functions like createTestUser() return plain objects with sensible defaults. Pass them to testDb.db.<model>.create({ data }) to persist them.

Previous: Rate Limit Service