Harness Integration Test
Service boundary testing, API contract verification, and consumer-driven contract validation. Ensures services communicate correctly without requiring full end-to-end infrastructure.
When to Use
- Testing API endpoints with real HTTP requests against a running service
- Validating consumer-driven contracts between microservices (Pact, Spring Cloud Contract)
- Verifying database interactions through repository or data access layers
- NOT when testing pure business logic with no I/O (use unit tests or harness-tdd instead)
- NOT when testing full user flows through a browser (use harness-e2e instead)
- NOT when performing load or stress testing on APIs (use harness-load-testing instead)
Process
Phase 1: DISCOVER -- Map Service Boundaries and Dependencies
Identify service boundaries. Scan the project structure for:
- API route definitions (Express routers, FastAPI endpoints, Spring controllers, Go HTTP handlers)
- Service client code (HTTP clients, gRPC stubs, message queue publishers/consumers)
- Shared type definitions or API schemas (OpenAPI specs, proto files, GraphQL schemas)
Map inter-service dependencies. For each service, catalog:
- Upstream dependencies: services this service calls
- Downstream consumers: services that call this service
- Shared resources: databases, message queues, caches
Inventory existing integration tests. Glob for test files in tests/integration/, __integration__/, tests/api/, and contract-tests/. Classify by type:
- API tests: send HTTP requests and assert responses
- Contract tests: verify provider/consumer agreements
- Repository tests: test data access against a real database
Identify coverage gaps. Cross-reference discovered endpoints and service boundaries against existing tests. Flag untested:
- API endpoints with no request/response validation
- Service boundaries with no contract tests
- Error scenarios (4xx responses, timeout handling, retry behavior)
Select test strategy. Based on the architecture:
- Monolith: API tests with supertest/httptest against the running application
- Microservices: consumer-driven contract tests with Pact plus API tests per service
- Event-driven: message contract tests plus async handler integration tests
Phase 2: MOCK -- Configure Test Doubles and Infrastructure
Set up test database. Choose the appropriate strategy:
- Testcontainers: Spin up a real database in Docker for each test suite. Preferred for PostgreSQL, MySQL, MongoDB.
- In-memory database: SQLite in-memory for lightweight tests. Only when schema compatibility is confirmed.
- Transaction rollback: Wrap each test in a transaction and roll back. Fast but requires careful connection management.
Configure mock services for external dependencies. For each upstream dependency:
- Create a mock server using the framework's built-in tools (Pact mock, WireMock, nock, MSW)
- Define request/response pairs from the API contract or OpenAPI spec
- Configure realistic error responses (500, 503, timeout) for error path testing
Set up contract broker (if using Pact). Configure:
- Pact broker URL and authentication
- Consumer and provider version tagging strategy
- Webhook configuration for provider verification on deploy
Create test fixtures and seed data. Generate:
- Database seed scripts for required reference data
- Request/response fixtures for common API payloads
- Factory functions for building test entities with sensible defaults
Verify mock infrastructure starts. Run a smoke test that:
- Starts the test database and confirms connectivity
- Starts mock services and confirms they respond
- Seeds baseline data and confirms it is queryable
Phase 3: IMPLEMENT -- Write Integration Tests
Write API endpoint tests. For each endpoint, test:
- Happy path: valid request returns expected response with correct status code and body
- Validation: invalid input returns 400 with descriptive error messages
- Authentication: unauthenticated requests return 401, unauthorized return 403
- Not found: requests for non-existent resources return 404
- Edge cases: empty collections, pagination boundaries, large payloads
Write consumer-driven contract tests (when applicable). For each consumer-provider pair:
- Consumer side: define interactions (request/response pairs) the consumer expects
- Provider side: verify the provider satisfies all consumer contracts
- Use Pact matchers for flexible verification (type matching, regex, array-like)
Write repository/data access tests. For each data access layer:
- CRUD operations with valid data
- Constraint violations (unique, foreign key, not-null)
- Query correctness (filters, sorting, pagination)
- Transaction behavior (isolation, rollback on error)
Write error handling and resilience tests. Verify:
- Timeout behavior: service responds within SLA when dependency is slow
- Retry logic: transient failures trigger retries with backoff
- Circuit breaker: repeated failures open the circuit and return fallback
- Graceful degradation: partial dependency failure does not crash the service
Organize tests by execution speed. Tag or separate:
- Fast integration tests (in-memory mocks, < 5 seconds): run on every commit
- Slow integration tests (testcontainers, external services, > 5 seconds): run on PR
Phase 4: VALIDATE -- Execute and Verify Contract Compliance
Run the full integration test suite. Execute all tests with verbose output. Collect:
- Pass/fail counts per test category (API, contract, repository)
- Execution time per test and per suite
- Any tests that require external services to be running
Verify contract compliance. If using Pact:
- Publish consumer pacts to the broker
- Run provider verification against published pacts
- Confirm the can-i-deploy check passes for the target environment
Validate test isolation. Run tests in random order (if the framework supports it). Any test that fails only when run after a specific other test has a shared-state bug. Fix immediately.
Run harness validate. Confirm the project passes all harness checks including the new integration test infrastructure.
Generate coverage report. Summarize:
- Endpoints tested vs. total endpoints discovered
- Contract coverage: consumer-provider pairs with verified contracts
- Error scenarios covered vs. identified
- Recommended next steps for remaining gaps
Graph Refresh
If a knowledge graph exists at .harness/graph/, refresh it after code changes to keep graph queries accurate:
harness scan [path]
Harness Integration
harness validate -- Run in VALIDATE phase after all integration tests are implemented. Confirms project-wide health.
harness check-deps -- Run after MOCK phase to verify test infrastructure dependencies do not leak into production bundles.
emit_interaction -- Used at checkpoints to present contract verification results and coverage gaps to the human.
- Grep -- Used in DISCOVER phase to find route definitions, HTTP client usage, and service boundary patterns.
- Glob -- Used to catalog existing integration tests and contract files.
Success Criteria
- Every API endpoint has at least one integration test covering the happy path
- Every consumer-provider boundary has a verified contract (when using microservices)
- Error scenarios (400, 401, 403, 404, 500, timeout) are tested for all public endpoints
- All integration tests pass with test doubles -- no dependency on external staging environments for CI
- Test isolation is verified: tests pass in any execution order
harness validate passes with the integration test suite in place
Examples
Example: Express API with Supertest and Testcontainers
DISCOVER output:
Framework: Express 4.18 with TypeScript
Database: PostgreSQL via Prisma
Endpoints: 14 routes across 4 controllers (users, projects, tasks, auth)
Existing tests: 3 integration tests in tests/integration/ (auth only)
Coverage gaps: projects CRUD, tasks filtering, user profile update
IMPLEMENT -- API endpoint test with supertest:
// tests/integration/projects.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import { prisma } from '../../src/db';
import { createTestUser, generateAuthToken } from '../helpers/auth';
describe('POST /api/projects', () => {
let authToken: string;
beforeAll(async () => {
const user = await createTestUser(prisma);
authToken = generateAuthToken(user.id);
});
afterAll(async () => {
await prisma.project.deleteMany();
await prisma.user.deleteMany();
});
it('creates a project with valid data', async () => {
const response = await request(app)
.post('/api/projects')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'Test Project', description: 'Integration test' });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
name: 'Test Project',
description: 'Integration test',
});
expect(response.body.id).toBeDefined();
});
it('returns 400 when name is missing', async () => {
const response = await request(app)
.post('/api/projects')
.set('Authorization', `Bearer ${authToken}`)
.send({ description: 'No name' });
expect(response.status).toBe(400);
expect(response.body.errors).toContainEqual(expect.objectContaining({ field: 'name' }));
});
it('returns 401 without auth token', async () => {
const response = await request(app).post('/api/projects').send({ name: 'Unauthorized' });
expect(response.status).toBe(401);
});
});
Example: Pact Consumer-Driven Contract Test
IMPLEMENT -- Consumer side (frontend):
// contract-tests/consumer/project-service.pact.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { ProjectClient } from '../../src/clients/project-client';
const { like, eachLike, uuid } = MatchersV3;
const provider = new PactV3({
consumer: 'Dashboard',
provider: 'ProjectService',
});
describe('ProjectService contract', () => {
it('returns a list of projects', async () => {
await provider
.given('projects exist for user')
.uponReceiving('a request for user projects')
.withRequest({
method: 'GET',
path: '/api/projects',
headers: { Authorization: like('Bearer token-123') },
})
.willRespondWith({
status: 200,
body: eachLike({
id: uuid(),
name: like('Project Alpha'),
createdAt: like('2026-01-15T10:30:00Z'),
}),
})
.executeTest(async (mockServer) => {
const client = new ProjectClient(mockServer.url);
const projects = await client.listProjects('token-123');
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe('Project Alpha');
});
});
});
Rationalizations to Reject
| Rationalization |
Why It Is Wrong |
| "Testing the happy path is sufficient -- error scenarios are edge cases" |
The success criteria require error scenarios (400, 401, 403, 404, 500, timeout) for all public endpoints. Error paths are where real-world failures happen. |
| "We can test against the staging environment instead of setting up local mocks" |
No integration tests that require external staging environments for CI. Tests must run with local test doubles. |
| "The consumer contract changed, so I will update the consumer test to match the provider" |
Contract changes must be coordinated. The provider may have introduced a bug, not an intentional change. |
| "Tests pass when I run them in order, so they are fine" |
Phase 4 requires running tests in random order. Any test that fails only in a specific order has a shared-state bug. |
Gates
- No integration tests that require external staging environments for CI. Every integration test must run with local test doubles (mocks, containers, in-memory databases). Tests that fail without a staging VPN are not integration tests -- they are environment tests.
- No shared mutable state between tests. Each test must set up and tear down its own data. If tests fail when run in random order, shared state exists. Fix it before proceeding.
- No testing implementation details. Integration tests assert on API contracts (status codes, response shapes, headers) and observable data changes -- not on internal function calls or database column values that are not part of the public contract.
- Contract changes must be coordinated. If a provider contract test reveals a breaking change, do not silently update the consumer expectation. Flag it as a coordination point between teams.
Escalation
- When a service dependency has no API documentation or schema: Cannot write accurate contract tests without knowing the contract. Escalate to the dependency team to provide an OpenAPI spec, proto file, or at minimum a Pact broker with published contracts.
- When Testcontainers fails in CI (Docker-in-Docker issues, resource limits): Fall back to in-memory alternatives where possible. For databases that have no in-memory mode, escalate to DevOps to configure CI runners with Docker support.
- When contract verification fails on the provider side: This indicates a real incompatibility between consumer expectations and provider implementation. Do not adjust the consumer test to match the provider bug. Escalate to the provider team with the failing interaction details.
- When integration tests exceed 5 minutes for a single service: Triage by separating fast tests (mocked dependencies) from slow tests (testcontainers). Run fast tests on every commit, slow tests on PR only.
1---2name: harness-integration-test3description: Harness Integration Test4---5# Harness Integration Test67> Service boundary testing, API contract verification, and consumer-driven contract validation. Ensures services communicate correctly without requiring full end-to-end infrastructure.89## When to Use1011- Testing API endpoints with real HTTP requests against a running service12- Validating consumer-driven contracts between microservices (Pact, Spring Cloud Contract)13- Verifying database interactions through repository or data access layers14- NOT when testing pure business logic with no I/O (use unit tests or harness-tdd instead)15- NOT when testing full user flows through a browser (use harness-e2e instead)16- NOT when performing load or stress testing on APIs (use harness-load-testing instead)1718## Process1920### Phase 1: DISCOVER -- Map Service Boundaries and Dependencies21221. **Identify service boundaries.** Scan the project structure for:23 - API route definitions (Express routers, FastAPI endpoints, Spring controllers, Go HTTP handlers)24 - Service client code (HTTP clients, gRPC stubs, message queue publishers/consumers)25 - Shared type definitions or API schemas (OpenAPI specs, proto files, GraphQL schemas)26272. **Map inter-service dependencies.** For each service, catalog:28 - Upstream dependencies: services this service calls29 - Downstream consumers: services that call this service30 - Shared resources: databases, message queues, caches31323. **Inventory existing integration tests.** Glob for test files in `tests/integration/`, `__integration__/`, `tests/api/`, and `contract-tests/`. Classify by type:33 - API tests: send HTTP requests and assert responses34 - Contract tests: verify provider/consumer agreements35 - Repository tests: test data access against a real database36374. **Identify coverage gaps.** Cross-reference discovered endpoints and service boundaries against existing tests. Flag untested:38 - API endpoints with no request/response validation39 - Service boundaries with no contract tests40 - Error scenarios (4xx responses, timeout handling, retry behavior)41425. **Select test strategy.** Based on the architecture:43 - Monolith: API tests with supertest/httptest against the running application44 - Microservices: consumer-driven contract tests with Pact plus API tests per service45 - Event-driven: message contract tests plus async handler integration tests4647### Phase 2: MOCK -- Configure Test Doubles and Infrastructure48491. **Set up test database.** Choose the appropriate strategy:50 - **Testcontainers:** Spin up a real database in Docker for each test suite. Preferred for PostgreSQL, MySQL, MongoDB.51 - **In-memory database:** SQLite in-memory for lightweight tests. Only when schema compatibility is confirmed.52 - **Transaction rollback:** Wrap each test in a transaction and roll back. Fast but requires careful connection management.53542. **Configure mock services for external dependencies.** For each upstream dependency:55 - Create a mock server using the framework's built-in tools (Pact mock, WireMock, nock, MSW)56 - Define request/response pairs from the API contract or OpenAPI spec57 - Configure realistic error responses (500, 503, timeout) for error path testing58593. **Set up contract broker (if using Pact).** Configure:60 - Pact broker URL and authentication61 - Consumer and provider version tagging strategy62 - Webhook configuration for provider verification on deploy63644. **Create test fixtures and seed data.** Generate:65 - Database seed scripts for required reference data66 - Request/response fixtures for common API payloads67 - Factory functions for building test entities with sensible defaults68695. **Verify mock infrastructure starts.** Run a smoke test that:70 - Starts the test database and confirms connectivity71 - Starts mock services and confirms they respond72 - Seeds baseline data and confirms it is queryable7374### Phase 3: IMPLEMENT -- Write Integration Tests75761. **Write API endpoint tests.** For each endpoint, test:77 - Happy path: valid request returns expected response with correct status code and body78 - Validation: invalid input returns 400 with descriptive error messages79 - Authentication: unauthenticated requests return 401, unauthorized return 40380 - Not found: requests for non-existent resources return 40481 - Edge cases: empty collections, pagination boundaries, large payloads82832. **Write consumer-driven contract tests (when applicable).** For each consumer-provider pair:84 - Consumer side: define interactions (request/response pairs) the consumer expects85 - Provider side: verify the provider satisfies all consumer contracts86 - Use Pact matchers for flexible verification (type matching, regex, array-like)87883. **Write repository/data access tests.** For each data access layer:89 - CRUD operations with valid data90 - Constraint violations (unique, foreign key, not-null)91 - Query correctness (filters, sorting, pagination)92 - Transaction behavior (isolation, rollback on error)93944. **Write error handling and resilience tests.** Verify:95 - Timeout behavior: service responds within SLA when dependency is slow96 - Retry logic: transient failures trigger retries with backoff97 - Circuit breaker: repeated failures open the circuit and return fallback98 - Graceful degradation: partial dependency failure does not crash the service991005. **Organize tests by execution speed.** Tag or separate:101 - Fast integration tests (in-memory mocks, < 5 seconds): run on every commit102 - Slow integration tests (testcontainers, external services, > 5 seconds): run on PR103104### Phase 4: VALIDATE -- Execute and Verify Contract Compliance1051061. **Run the full integration test suite.** Execute all tests with verbose output. Collect:107 - Pass/fail counts per test category (API, contract, repository)108 - Execution time per test and per suite109 - Any tests that require external services to be running1101112. **Verify contract compliance.** If using Pact:112 - Publish consumer pacts to the broker113 - Run provider verification against published pacts114 - Confirm the can-i-deploy check passes for the target environment1151163. **Validate test isolation.** Run tests in random order (if the framework supports it). Any test that fails only when run after a specific other test has a shared-state bug. Fix immediately.1171184. **Run `harness validate`.** Confirm the project passes all harness checks including the new integration test infrastructure.1191205. **Generate coverage report.** Summarize:121 - Endpoints tested vs. total endpoints discovered122 - Contract coverage: consumer-provider pairs with verified contracts123 - Error scenarios covered vs. identified124 - Recommended next steps for remaining gaps125126### Graph Refresh127128If a knowledge graph exists at `.harness/graph/`, refresh it after code changes to keep graph queries accurate:129130```131harness scan [path]132```133134## Harness Integration135136- **`harness validate`** -- Run in VALIDATE phase after all integration tests are implemented. Confirms project-wide health.137- **`harness check-deps`** -- Run after MOCK phase to verify test infrastructure dependencies do not leak into production bundles.138- **`emit_interaction`** -- Used at checkpoints to present contract verification results and coverage gaps to the human.139- **Grep** -- Used in DISCOVER phase to find route definitions, HTTP client usage, and service boundary patterns.140- **Glob** -- Used to catalog existing integration tests and contract files.141142## Success Criteria143144- Every API endpoint has at least one integration test covering the happy path145- Every consumer-provider boundary has a verified contract (when using microservices)146- Error scenarios (400, 401, 403, 404, 500, timeout) are tested for all public endpoints147- All integration tests pass with test doubles -- no dependency on external staging environments for CI148- Test isolation is verified: tests pass in any execution order149- `harness validate` passes with the integration test suite in place150151## Examples152153### Example: Express API with Supertest and Testcontainers154155**DISCOVER output:**156157```158Framework: Express 4.18 with TypeScript159Database: PostgreSQL via Prisma160Endpoints: 14 routes across 4 controllers (users, projects, tasks, auth)161Existing tests: 3 integration tests in tests/integration/ (auth only)162Coverage gaps: projects CRUD, tasks filtering, user profile update163```164165**IMPLEMENT -- API endpoint test with supertest:**166167```typescript168// tests/integration/projects.test.ts169import request from 'supertest';170import { app } from '../../src/app';171import { prisma } from '../../src/db';172import { createTestUser, generateAuthToken } from '../helpers/auth';173174describe('POST /api/projects', () => {175 let authToken: string;176177 beforeAll(async () => {178 const user = await createTestUser(prisma);179 authToken = generateAuthToken(user.id);180 });181182 afterAll(async () => {183 await prisma.project.deleteMany();184 await prisma.user.deleteMany();185 });186187 it('creates a project with valid data', async () => {188 const response = await request(app)189 .post('/api/projects')190 .set('Authorization', `Bearer ${authToken}`)191 .send({ name: 'Test Project', description: 'Integration test' });192193 expect(response.status).toBe(201);194 expect(response.body).toMatchObject({195 name: 'Test Project',196 description: 'Integration test',197 });198 expect(response.body.id).toBeDefined();199 });200201 it('returns 400 when name is missing', async () => {202 const response = await request(app)203 .post('/api/projects')204 .set('Authorization', `Bearer ${authToken}`)205 .send({ description: 'No name' });206207 expect(response.status).toBe(400);208 expect(response.body.errors).toContainEqual(expect.objectContaining({ field: 'name' }));209 });210211 it('returns 401 without auth token', async () => {212 const response = await request(app).post('/api/projects').send({ name: 'Unauthorized' });213214 expect(response.status).toBe(401);215 });216});217```218219### Example: Pact Consumer-Driven Contract Test220221**IMPLEMENT -- Consumer side (frontend):**222223```typescript224// contract-tests/consumer/project-service.pact.ts225import { PactV3, MatchersV3 } from '@pact-foundation/pact';226import { ProjectClient } from '../../src/clients/project-client';227228const { like, eachLike, uuid } = MatchersV3;229230const provider = new PactV3({231 consumer: 'Dashboard',232 provider: 'ProjectService',233});234235describe('ProjectService contract', () => {236 it('returns a list of projects', async () => {237 await provider238 .given('projects exist for user')239 .uponReceiving('a request for user projects')240 .withRequest({241 method: 'GET',242 path: '/api/projects',243 headers: { Authorization: like('Bearer token-123') },244 })245 .willRespondWith({246 status: 200,247 body: eachLike({248 id: uuid(),249 name: like('Project Alpha'),250 createdAt: like('2026-01-15T10:30:00Z'),251 }),252 })253 .executeTest(async (mockServer) => {254 const client = new ProjectClient(mockServer.url);255 const projects = await client.listProjects('token-123');256 expect(projects).toHaveLength(1);257 expect(projects[0].name).toBe('Project Alpha');258 });259 });260});261```262263## Rationalizations to Reject264265| Rationalization | Why It Is Wrong |266| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |267| "Testing the happy path is sufficient -- error scenarios are edge cases" | The success criteria require error scenarios (400, 401, 403, 404, 500, timeout) for all public endpoints. Error paths are where real-world failures happen. |268| "We can test against the staging environment instead of setting up local mocks" | No integration tests that require external staging environments for CI. Tests must run with local test doubles. |269| "The consumer contract changed, so I will update the consumer test to match the provider" | Contract changes must be coordinated. The provider may have introduced a bug, not an intentional change. |270| "Tests pass when I run them in order, so they are fine" | Phase 4 requires running tests in random order. Any test that fails only in a specific order has a shared-state bug. |271272## Gates273274- **No integration tests that require external staging environments for CI.** Every integration test must run with local test doubles (mocks, containers, in-memory databases). Tests that fail without a staging VPN are not integration tests -- they are environment tests.275- **No shared mutable state between tests.** Each test must set up and tear down its own data. If tests fail when run in random order, shared state exists. Fix it before proceeding.276- **No testing implementation details.** Integration tests assert on API contracts (status codes, response shapes, headers) and observable data changes -- not on internal function calls or database column values that are not part of the public contract.277- **Contract changes must be coordinated.** If a provider contract test reveals a breaking change, do not silently update the consumer expectation. Flag it as a coordination point between teams.278279## Escalation280281- **When a service dependency has no API documentation or schema:** Cannot write accurate contract tests without knowing the contract. Escalate to the dependency team to provide an OpenAPI spec, proto file, or at minimum a Pact broker with published contracts.282- **When Testcontainers fails in CI (Docker-in-Docker issues, resource limits):** Fall back to in-memory alternatives where possible. For databases that have no in-memory mode, escalate to DevOps to configure CI runners with Docker support.283- **When contract verification fails on the provider side:** This indicates a real incompatibility between consumer expectations and provider implementation. Do not adjust the consumer test to match the provider bug. Escalate to the provider team with the failing interaction details.284- **When integration tests exceed 5 minutes for a single service:** Triage by separating fast tests (mocked dependencies) from slow tests (testcontainers). Run fast tests on every commit, slow tests on PR only.