install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/test-integration-patterns" ~/.claude/skills/intense-visions-harness-engineering-test-integration-patterns-63c9ba && rm -rf "$T"
manifest:
agents/skills/claude-code/test-integration-patterns/SKILL.mdsource content
Test Integration Patterns
Write integration tests that exercise real dependencies using test databases and containers
When to Use
- Testing service layers with real database connections
- Verifying API endpoint behavior with actual HTTP requests
- Testing multi-component workflows end-to-end within the backend
- Validating that modules work correctly when integrated together
Instructions
- Use a test database — run a real database for integration tests:
// test/setup.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient({ datasources: { db: { url: process.env.TEST_DATABASE_URL } }, }); beforeEach(async () => { // Clean tables in dependency order await prisma.post.deleteMany(); await prisma.user.deleteMany(); }); afterAll(async () => { await prisma.$disconnect(); }); export { prisma };
- Test containers for disposable databases:
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql'; let container: StartedPostgreSqlContainer; beforeAll(async () => { container = await new PostgreSqlContainer().start(); process.env.DATABASE_URL = container.getConnectionUri(); // Run migrations await execSync('npx prisma migrate deploy'); }, 60_000); // Container startup timeout afterAll(async () => { await container.stop(); });
- Test API endpoints with supertest or the framework's test client:
import request from 'supertest'; import { app } from '../app'; describe('POST /api/users', () => { it('creates a user and returns 201', async () => { const response = await request(app) .post('/api/users') .send({ name: 'Alice', email: 'alice@test.com' }) .expect(201); expect(response.body).toMatchObject({ name: 'Alice', email: 'alice@test.com', }); expect(response.body.id).toBeDefined(); }); it('returns 400 for invalid email', async () => { await request(app).post('/api/users').send({ name: 'Alice', email: 'invalid' }).expect(400); }); });
- Transaction-based isolation — wrap each test in a transaction that rolls back:
import { prisma } from './setup'; let tx: PrismaClient; beforeEach(async () => { // Start a transaction for test isolation // Each test sees a clean state without deleting data await prisma.$executeRaw`BEGIN`; }); afterEach(async () => { await prisma.$executeRaw`ROLLBACK`; });
- Test service layer with real dependencies:
describe('OrderService', () => { it('creates order and deducts inventory', async () => { // Arrange — seed test data const product = await prisma.product.create({ data: { name: 'Widget', stock: 10, price: 9.99 }, }); const service = new OrderService(prisma); // Act const order = await service.createOrder({ items: [{ productId: product.id, quantity: 3 }], userId: testUser.id, }); // Assert — verify both order creation AND inventory deduction expect(order.total).toBe(29.97); const updatedProduct = await prisma.product.findUnique({ where: { id: product.id }, }); expect(updatedProduct!.stock).toBe(7); }); });
- Use factory helpers for test data:
async function createTestUser(overrides?: Partial<User>) { return prisma.user.create({ data: { email: `test-${crypto.randomUUID()}@test.com`, name: 'Test User', ...overrides, }, }); }
- Separate test configuration:
// vitest.config.integration.ts export default defineConfig({ test: { include: ['**/*.integration.test.ts'], setupFiles: ['./test/integration-setup.ts'], testTimeout: 30_000, hookTimeout: 60_000, }, });
Run with:
vitest --config vitest.config.integration.ts
Details
Integration tests verify that multiple components work together correctly. They catch issues that unit tests miss — serialization bugs, query errors, constraint violations, and middleware ordering problems.
Test database strategies:
- Shared test database — fast setup, requires cleanup between tests. Best for local development
- Test containers — disposable per-suite database. Slower startup but perfect isolation. Best for CI
- SQLite in-memory — fastest but behavior may differ from production database (PostgreSQL, MySQL)
Data isolation approaches:
- Delete-and-reseed — simple but slow for large datasets. Use
in reverse dependency orderdeleteMany - Transaction rollback — fast, no data ever written. But cannot test transaction behavior itself
- Separate schemas/databases — full isolation per test suite. Slowest setup but safest
Integration vs E2E: Integration tests exercise the backend (service + database) without a browser. E2E tests include the full stack (browser + backend + database). Integration tests are faster and more focused.
Trade-offs:
- Real databases catch real bugs (constraint violations, query errors) — but are slower than mocked tests
- Test containers provide perfect isolation — but add 5-15 seconds of startup time per suite
- Transaction rollback is fast — but prevents testing transaction behavior and concurrent access
- Integration tests catch more bugs per test — but are harder to maintain and debug when they fail
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.