# Testing Utilities

> Database testing utilities for unit and integration tests in the TanStack Start Prisma SaaS Kit.

*Canonical: https://makerkit.dev/docs/tanstack-prisma/database/testing*

---

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](./overview) 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`:

```typescript
// 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:

```typescript
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

```typescript
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`

{% callout type="warning" title="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.
{% /callout %}

## Factory Functions

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

### createTestUser()

```typescript
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:**

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

```typescript
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()

```typescript
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()

```typescript
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:

```typescript
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):

```typescript
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:

```typescript
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

```typescript
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:

```typescript {% title="vitest.config.ts" %}
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['**/*.test.ts'],
  },
});
```

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "What is PGlite and why is it used for testing?", "answer": "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."},
     {"question": "Why does cleanup() use TRUNCATE instead of DELETE?", "answer": "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."},
     {"question": "How do I test multi-tenant authorization?", "answer": "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."},
     {"question": "Do factories write to the database?", "answer": "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](./rate-limit-service)
