Files
gh-djankies-claude-configs-…/skills/creating-client-singletons/references/test-pattern.md
2025-11-29 18:22:25 +08:00

5.1 KiB

Test Pattern

Proper test setup with PrismaClient singleton ensures test isolation and prevents connection exhaustion.

Test File Setup

Import singleton, don't create:

import { prisma } from '@/lib/prisma'

describe('User operations', () => {
  beforeEach(async () => {
    await prisma.user.deleteMany()
  })

  it('creates user', async () => {
    const user = await prisma.user.create({
      data: { email: 'test@example.com' }
    })
    expect(user.email).toBe('test@example.com')
  })

  afterAll(async () => {
    await prisma.$disconnect()
  })
})

Key Points:

  • Import singleton, don't create
  • Clean state with deleteMany or transactions
  • Disconnect once at end of suite
  • Don't disconnect between tests (kills connection pool)

Test Isolation with Transactions

Better approach for test isolation:

import { prisma } from '@/lib/prisma'
import { PrismaClient } from '@prisma/client'

describe('User operations', () => {
  let testPrisma: Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>

  beforeEach(async () => {
    await prisma.$transaction(async (tx) => {
      testPrisma = tx
      await tx.user.deleteMany()
    })
  })

  it('creates user', async () => {
    const user = await testPrisma.user.create({
      data: { email: 'test@example.com' }
    })
    expect(user.email).toBe('test@example.com')
  })
})

Why this works:

  • Each test runs in transaction
  • Automatic rollback after test
  • No data leakage between tests
  • Faster than deleteMany

Mocking PrismaClient for Unit Tests

When to mock:

  • Testing business logic without database
  • Fast unit tests
  • CI/CD pipeline optimization

File: __mocks__/prisma.ts

import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'

export const prismaMock = mockDeep<PrismaClient>()

beforeEach(() => {
  mockReset(prismaMock)
})

File: __tests__/userService.test.ts

import { prismaMock } from '../__mocks__/prisma'
import { createUser } from '../services/userService'

jest.mock('@/lib/prisma', () => ({
  __esModule: true,
  default: prismaMock,
}))

describe('User Service', () => {
  it('creates user with email', async () => {
    const mockUser = { id: '1', email: 'test@example.com' }

    prismaMock.user.create.mockResolvedValue(mockUser)

    const user = await createUser('test@example.com')

    expect(user.email).toBe('test@example.com')
    expect(prismaMock.user.create).toHaveBeenCalledWith({
      data: { email: 'test@example.com' }
    })
  })
})

Key Points:

  • Mock the singleton module, not PrismaClient
  • Reset mocks between tests
  • Type-safe mocks with jest-mock-extended
  • Fast tests without database

Integration Test Setup

File: tests/setup.ts

import { prisma } from '@/lib/prisma'

beforeAll(async () => {
  await prisma.$connect()
})

afterAll(async () => {
  await prisma.$disconnect()
})

export async function cleanDatabase() {
  const tables = ['User', 'Post', 'Comment']

  for (const table of tables) {
    await prisma[table.toLowerCase()].deleteMany()
  }
}

File: tests/users.integration.test.ts

import { prisma } from '@/lib/prisma'
import { cleanDatabase } from './setup'

describe('User Integration Tests', () => {
  beforeEach(async () => {
    await cleanDatabase()
  })

  it('creates and retrieves user', async () => {
    const created = await prisma.user.create({
      data: { email: 'test@example.com' }
    })

    const retrieved = await prisma.user.findUnique({
      where: { id: created.id }
    })

    expect(retrieved?.email).toBe('test@example.com')
  })

  it('handles unique constraint', async () => {
    await prisma.user.create({
      data: { email: 'test@example.com' }
    })

    await expect(
      prisma.user.create({
        data: { email: 'test@example.com' }
      })
    ).rejects.toThrow(/Unique constraint/)
  })
})

Key Points:

  • Shared setup in tests/setup.ts
  • Clean database between tests
  • Test real database behavior
  • Catch constraint violations

Anti-Pattern: Creating Client in Tests

WRONG:

import { PrismaClient } from '@prisma/client'

describe('User tests', () => {
  let prisma: PrismaClient

  beforeEach(() => {
    prisma = new PrismaClient()
  })

  afterEach(async () => {
    await prisma.$disconnect()
  })

  it('creates user', async () => {
    const user = await prisma.user.create({
      data: { email: 'test@example.com' }
    })
    expect(user.email).toBe('test@example.com')
  })
})

Problems:

  • New connection pool every test
  • Connect/disconnect overhead
  • Connection exhaustion in large suites
  • Slow tests

Fix:

import { prisma } from '@/lib/prisma'

describe('User tests', () => {
  beforeEach(async () => {
    await prisma.user.deleteMany()
  })

  it('creates user', async () => {
    const user = await prisma.user.create({
      data: { email: 'test@example.com' }
    })
    expect(user.email).toBe('test@example.com')
  })
})

Result:

  • Reuses singleton connection
  • Fast tests
  • No connection exhaustion