140 lines
3.3 KiB
Markdown
140 lines
3.3 KiB
Markdown
# Database Testing
|
|
|
|
## Testcontainers (Real Database Instances)
|
|
|
|
### Setup
|
|
|
|
```bash
|
|
npm install -D @testcontainers/postgresql
|
|
# or
|
|
npm install -D @testcontainers/mongodb
|
|
```
|
|
|
|
### PostgreSQL Example
|
|
|
|
```typescript
|
|
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
|
import { Pool } from 'pg';
|
|
|
|
describe('User Repository', () => {
|
|
let container: PostgreSqlContainer;
|
|
let pool: Pool;
|
|
|
|
beforeAll(async () => {
|
|
container = await new PostgreSqlContainer()
|
|
.withDatabase('testdb')
|
|
.start();
|
|
|
|
pool = new Pool({ connectionString: container.getConnectionUri() });
|
|
await runMigrations(pool);
|
|
}, 60000); // 60s timeout for container start
|
|
|
|
afterAll(async () => {
|
|
await pool.end();
|
|
await container.stop();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await pool.query('TRUNCATE users RESTART IDENTITY CASCADE');
|
|
});
|
|
|
|
it('creates user', async () => {
|
|
const repo = new UserRepository(pool);
|
|
const user = await repo.create({ email: 'test@example.com' });
|
|
expect(user.id).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
### MongoDB Example
|
|
|
|
```typescript
|
|
import { MongoDBContainer } from '@testcontainers/mongodb';
|
|
import mongoose from 'mongoose';
|
|
|
|
describe('User Repository', () => {
|
|
let container: MongoDBContainer;
|
|
|
|
beforeAll(async () => {
|
|
container = await new MongoDBContainer().start();
|
|
await mongoose.connect(container.getConnectionString(), {
|
|
directConnection: true,
|
|
});
|
|
}, 60000);
|
|
|
|
afterAll(async () => {
|
|
await mongoose.disconnect();
|
|
await container.stop();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await mongoose.connection.dropDatabase();
|
|
});
|
|
});
|
|
```
|
|
|
|
## Transaction Rollback Pattern
|
|
|
|
```typescript
|
|
describe('User Service', () => {
|
|
let transaction: Transaction;
|
|
|
|
beforeEach(async () => {
|
|
transaction = await db.transaction();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await transaction.rollback(); // Always rollback
|
|
});
|
|
|
|
it('creates user within transaction', async () => {
|
|
const service = new UserService(transaction);
|
|
await service.create({ email: 'test@example.com' });
|
|
// Transaction rolls back - no cleanup needed
|
|
});
|
|
});
|
|
```
|
|
|
|
## Playwright Database Fixture
|
|
|
|
```typescript
|
|
// fixtures/db.ts
|
|
import { test as base } from '@playwright/test';
|
|
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
|
|
|
export const test = base.extend<{ db: Pool }>({
|
|
db: [async ({}, use, testInfo) => {
|
|
const container = await new PostgreSqlContainer().start();
|
|
const pool = new Pool({ connectionString: container.getConnectionUri() });
|
|
|
|
// Seed per-worker data
|
|
await seedData(pool, testInfo.workerIndex);
|
|
|
|
await use(pool);
|
|
|
|
await pool.end();
|
|
await container.stop();
|
|
}, { scope: 'worker' }]
|
|
});
|
|
```
|
|
|
|
## In-Memory Alternatives
|
|
|
|
```typescript
|
|
// SQLite in-memory (faster, less realistic)
|
|
const db = new Database(':memory:');
|
|
|
|
// PGlite (Postgres in browser/Node)
|
|
import { PGlite } from '@electric-sql/pglite';
|
|
const db = new PGlite();
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
- **Real DB in CI:** Use Testcontainers for high fidelity
|
|
- **In-memory locally:** Faster iteration during development
|
|
- **Isolation:** Worker-scoped containers for parallel tests
|
|
- **Migrations:** Always run migrations before tests
|
|
- **Cleanup:** Truncate after each test, stop containers after all
|
|
- **Timeouts:** Increase timeout for container startup (60s)
|