Integration Test Builder
Build comprehensive integration tests for APIs and database flows.
Test Harness Setup
// tests/setup/test-harness.ts import { PrismaClient } from "@prisma/client"; import { execSync } from "child_process";
export class TestHarness { prisma: PrismaClient;
async setup() { // Setup test database process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
// Run migrations
execSync("npx prisma migrate deploy");
// Initialize Prisma client
this.prisma = new PrismaClient();
// Clear all data
await this.clearDatabase();
}
async teardown() { await this.prisma.$disconnect(); }
async clearDatabase() {
const tables = await this.prisma.$queryRaw<{ tablename: string }[]> SELECT tablename FROM pg_tables WHERE schemaname = 'public' ;
for (const { tablename } of tables) {
if (tablename !== "_prisma_migrations") {
await this.prisma.$executeRawUnsafe(
`TRUNCATE TABLE "${tablename}" CASCADE`
);
}
}
}
async seedFixtures() { // Seed test data await this.prisma.user.create({ data: { email: "test@example.com", name: "Test User", }, }); } }
API Integration Tests
// tests/api/users.test.ts import request from "supertest"; import { app } from "@/app"; import { TestHarness } from "../setup/test-harness";
describe("User API", () => { let harness: TestHarness;
beforeAll(async () => { harness = new TestHarness(); await harness.setup(); });
afterAll(async () => { await harness.teardown(); });
beforeEach(async () => { await harness.clearDatabase(); await harness.seedFixtures(); });
describe("POST /api/users", () => { it("should create new user", async () => { // Arrange const userData = { email: "new@example.com", name: "New User", };
// Act
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(201);
// Assert
expect(response.body).toMatchObject({
email: userData.email,
name: userData.name,
});
expect(response.body.id).toBeDefined();
// Verify in database
const user = await harness.prisma.user.findUnique({
where: { email: userData.email },
});
expect(user).toBeDefined();
expect(user!.name).toBe(userData.name);
});
it("should return 400 for invalid email", async () => {
// Arrange
const userData = {
email: "invalid-email",
name: "Test User",
};
// Act
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(400);
// Assert
expect(response.body.error).toContain("Invalid email");
});
it("should return 409 for duplicate email", async () => {
// Arrange
const userData = {
email: "test@example.com", // Already exists
name: "Duplicate User",
};
// Act
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(409);
// Assert
expect(response.body.error).toContain("already exists");
});
});
describe("GET /api/users/:id", () => { it("should get user by id", async () => { // Arrange const user = await harness.prisma.user.findFirst();
// Act
const response = await request(app)
.get(`/api/users/${user!.id}`)
.expect(200);
// Assert
expect(response.body).toMatchObject({
id: user!.id,
email: user!.email,
name: user!.name,
});
});
it("should return 404 for non-existent user", async () => {
// Act
const response = await request(app).get("/api/users/99999").expect(404);
// Assert
expect(response.body.error).toContain("not found");
});
});
describe("PUT /api/users/:id", () => { it("should update user", async () => { // Arrange const user = await harness.prisma.user.findFirst(); const updates = { name: "Updated Name" };
// Act
const response = await request(app)
.put(`/api/users/${user!.id}`)
.send(updates)
.expect(200);
// Assert
expect(response.body.name).toBe("Updated Name");
// Verify in database
const updatedUser = await harness.prisma.user.findUnique({
where: { id: user!.id },
});
expect(updatedUser!.name).toBe("Updated Name");
});
});
describe("DELETE /api/users/:id", () => { it("should delete user", async () => { // Arrange const user = await harness.prisma.user.findFirst();
// Act
await request(app).delete(`/api/users/${user!.id}`).expect(204);
// Assert - verify deletion in database
const deletedUser = await harness.prisma.user.findUnique({
where: { id: user!.id },
});
expect(deletedUser).toBeNull();
});
}); });
Database Transaction Tests
// tests/integration/order-flow.test.ts describe("Order Flow", () => { it("should create order with items in transaction", async () => { // Arrange const user = await harness.prisma.user.findFirst(); const product = await harness.prisma.product.create({ data: { name: "Test Product", price: 99.99, stock: 10, }, });
const orderData = {
userId: user!.id,
items: [
{
productId: product.id,
quantity: 2,
price: product.price,
},
],
};
// Act
const response = await request(app)
.post("/api/orders")
.send(orderData)
.expect(201);
// Assert
const order = await harness.prisma.order.findUnique({
where: { id: response.body.id },
include: { items: true },
});
expect(order).toBeDefined();
expect(order!.items).toHaveLength(1);
expect(order!.items[0].quantity).toBe(2);
// Verify stock was decremented
const updatedProduct = await harness.prisma.product.findUnique({
where: { id: product.id },
});
expect(updatedProduct!.stock).toBe(8); // 10 - 2
});
it("should rollback transaction if order creation fails", async () => { // Arrange const user = await harness.prisma.user.findFirst(); const product = await harness.prisma.product.create({ data: { name: "Test Product", price: 99.99, stock: 1, // Only 1 in stock }, });
const orderData = {
userId: user!.id,
items: [
{
productId: product.id,
quantity: 10, // Requesting more than available
price: product.price,
},
],
};
// Act
await request(app).post("/api/orders").send(orderData).expect(400);
// Assert - verify rollback
const orders = await harness.prisma.order.findMany();
expect(orders).toHaveLength(0);
// Verify stock unchanged
const unchangedProduct = await harness.prisma.product.findUnique({
where: { id: product.id },
});
expect(unchangedProduct!.stock).toBe(1);
}); });
Authentication Tests
// tests/integration/auth.test.ts describe("Authentication", () => { describe("POST /api/auth/login", () => { it("should login with valid credentials", async () => { // Arrange await harness.prisma.user.create({ data: { email: "auth@example.com", password: await hash("password123"), }, });
// Act
const response = await request(app)
.post("/api/auth/login")
.send({
email: "auth@example.com",
password: "password123",
})
.expect(200);
// Assert
expect(response.body.token).toBeDefined();
expect(response.body.user.email).toBe("auth@example.com");
});
it("should reject invalid password", async () => {
// Act
const response = await request(app)
.post("/api/auth/login")
.send({
email: "test@example.com",
password: "wrong-password",
})
.expect(401);
// Assert
expect(response.body.error).toContain("Invalid credentials");
});
});
describe("Protected routes", () => { let authToken: string;
beforeEach(async () => {
// Login to get token
const response = await request(app).post("/api/auth/login").send({
email: "test@example.com",
password: "password123",
});
authToken = response.body.token;
});
it("should access protected route with valid token", async () => {
await request(app)
.get("/api/profile")
.set("Authorization", `Bearer ${authToken}`)
.expect(200);
});
it("should reject request without token", async () => {
await request(app).get("/api/profile").expect(401);
});
it("should reject request with invalid token", async () => {
await request(app)
.get("/api/profile")
.set("Authorization", "Bearer invalid-token")
.expect(401);
});
}); });
Fixtures Management
// tests/fixtures/users.ts export const userFixtures = { admin: { email: "admin@example.com", name: "Admin User", role: "ADMIN", }, regularUser: { email: "user@example.com", name: "Regular User", role: "USER", }, testUser: { email: "test@example.com", name: "Test User", role: "USER", }, };
// tests/fixtures/products.ts export const productFixtures = { laptop: { name: "MacBook Pro", price: 2499.99, stock: 10, category: "Electronics", }, phone: { name: "iPhone 15", price: 999.99, stock: 50, category: "Electronics", }, };
// Usage in tests await harness.prisma.user.create({ data: userFixtures.admin, });
CI-Friendly Strategy
.github/workflows/integration-tests.yml
name: Integration Tests
on: [push, pull_request]
services: postgres: image: postgres:15 env: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: test_db ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
jobs: test: runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test_db
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test_db
Parallel Test Execution
// vitest.config.ts export default defineConfig({ test: { pool: "forks", poolOptions: { forks: { singleFork: false, // Run tests in parallel }, }, isolate: true, // Isolate each test file setupFiles: ["./tests/setup/global-setup.ts"], }, });
// Ensure each test file uses separate database const TEST_DB_PREFIX = "test_db_";
function getDatabaseUrl(): string {
const workerId = process.env.VITEST_WORKER_ID || "1";
return postgresql://test:test@localhost:5432/${TEST_DB_PREFIX}${workerId};
}
Best Practices
-
Isolated tests: Each test can run independently
-
Clean state: Clear database between tests
-
Fast fixtures: Minimal data seeding
-
Transactions: Test rollbacks explicitly
-
Real database: Don't mock database in integration tests
-
CI-ready: Use Docker containers
-
Parallel execution: Independent test databases
Output Checklist
-
Test harness created
-
Database setup/teardown
-
Fixture management
-
API endpoint tests
-
Database transaction tests
-
Authentication tests
-
Error case coverage
-
CI workflow configured
-
Parallel execution support
-
Clear test naming