Initial commit
This commit is contained in:
259
skills/creating-client-singletons/references/test-pattern.md
Normal file
259
skills/creating-client-singletons/references/test-pattern.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Test Pattern
|
||||
|
||||
Proper test setup with PrismaClient singleton ensures test isolation and prevents connection exhaustion.
|
||||
|
||||
## Test File Setup
|
||||
|
||||
**Import singleton, don't create:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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`**
|
||||
|
||||
```typescript
|
||||
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`**
|
||||
|
||||
```typescript
|
||||
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`**
|
||||
|
||||
```typescript
|
||||
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`**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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
|
||||
Reference in New Issue
Block a user