Local Service Testing
Overview
Test against real services locally before pushing. CI validates—it doesn't discover.
Core principle: If you mock what you can run locally, you're hiding bugs.
Announce at start: "I'm using local-service-testing to verify changes against real services."
The Iron Law
CI DISCOVERS NOTHING.
IF CI FINDS A BUG, YOUR LOCAL TESTING FAILED.
Local services exist for a reason. Use them.
Critical Clarification
Unit tests with mocks: REQUIRED (for TDD cycle) Integration tests with real services: ALSO REQUIRED
This skill does NOT replace mocking in unit tests. It ADDS the requirement for integration tests against real services. Both are mandatory.
When This Skill Applies
| Code Change | Required Service | Must Test Against | |-------------|------------------|-------------------| | Database models/entities | postgres | Real postgres | | Migrations | postgres | Real postgres | | Repository/ORM layer | postgres | Real postgres | | SQL queries | postgres | Real postgres | | Cache operations | redis | Real redis | | Session storage | redis | Real redis | | Pub/sub messages | redis/rabbitmq | Real queue | | Queue workers | redis/rabbitmq | Real queue | | API endpoints | all services | All running |
If your change touches any of these, you MUST test against the real service.
Service Detection
At session start, the session-start.sh hook reports available services:
Checking development services...
✓ Found docker-compose.yml
Available services:
✓ postgres (running)
○ redis (not running)
Tip: Start services with: docker-compose up -d
Starting Services
# Start all services
docker-compose up -d
# Start specific service
docker-compose up -d postgres
# Check status
docker-compose ps
Service Connection Strings
| Service | Default Connection |
|---------|-------------------|
| postgres | postgresql://localhost:5432/dev |
| redis | redis://localhost:6379 |
| rabbitmq | amqp://localhost:5672 |
Check your project's .env.example or docker-compose.yml for actual values.
Testing Protocol
Step 1: Identify Service Dependencies
Before testing, identify which services your changes require:
# Check what files you've changed
git diff --name-only HEAD~1
# Map to services:
# *.sql, *migration*, *model*, *entity*, *repository* → postgres
# *cache*, *redis*, *session*, *queue*, *pub*, *sub* → redis
# *worker*, *job*, *consumer* → queue service
Step 2: Ensure Services Running
# Start required services
docker-compose up -d postgres redis
# Verify they're ready
docker-compose ps
# Test connectivity
# Postgres
psql postgresql://localhost:5432/dev -c "SELECT 1"
# Redis
redis-cli ping
Step 3: Run Integration Tests
# Run integration tests (not unit tests with mocks)
pnpm test:integration
# Or run specific integration test suite
pnpm test --grep "integration"
# For Python projects
pytest tests/integration/
# For Go projects
go test -tags=integration ./...
Step 4: Verify Locally Before Pushing
Before git push:
# Full verification
pnpm build
pnpm lint
pnpm typecheck
pnpm test # Unit tests
pnpm test:integration # Integration tests against real services
Two-Layer Testing Requirement
Both Are Required
| Test Layer | Purpose | Uses Mocks? | Uses Real Services? | Required? | |------------|---------|-------------|---------------------|-----------| | Unit tests | TDD cycle, verify logic | YES | No | YES | | Integration tests | Verify real behavior | No | YES | YES |
Why Both?
- Unit tests with mocks: Fast, enable RED-GREEN-REFACTOR, isolate logic
- Integration tests with services: Catch real-world failures mocks miss
We've experienced 80% failure rates with ORM migrations because unit tests with mocks passed but real databases rejected the changes.
What Mocks Miss
// Mock says this works
const mockDb = {
query: jest.fn().mockResolvedValue([{ id: 1 }])
};
// But real postgres throws:
// ERROR: relation "users" does not exist
// ERROR: column "email" cannot be null
// ERROR: duplicate key violates unique constraint
Real services reveal:
- Schema mismatches
- Constraint violations
- Connection issues
- Transaction behavior
- Performance problems
Artifact Requirement
Before creating a PR, you must post local testing evidence to the issue.
Required Artifact Format
<!-- LOCAL-TESTING:START -->
## Local Service Testing
| Service | Status | Verification |
|---------|--------|--------------|
| postgres | ✅ Running | Migrations applied, queries executed |
| redis | ✅ Running | Cache operations verified |
**Tests Run:**
- `pnpm test:integration` - PASSED
- Manual verification of [specific feature]
**Tested At:** 2025-01-15T10:30:00Z
<!-- LOCAL-TESTING:END -->
Where to Post
Post as a comment on the GitHub issue you're working on. This is checked by the validate-local-testing.sh PreToolUse hook before PR creation.
When Artifact is Required
The hook checks:
- Does docker-compose.yml exist?
- Do changed files match service patterns?
- If yes to both → artifact required
If no services are relevant to your changes, no artifact is needed.
Common Patterns
Database Testing
// GOOD: Test against real postgres
describe('UserRepository (integration)', () => {
beforeAll(async () => {
await db.migrate.latest();
});
afterAll(async () => {
await db.destroy();
});
it('creates user with unique email constraint', async () => {
await userRepo.create({ email: 'test@example.com' });
// Real postgres will throw on duplicate
await expect(
userRepo.create({ email: 'test@example.com' })
).rejects.toThrow(/unique constraint/);
});
});
Cache Testing
// GOOD: Test against real redis
describe('CacheService (integration)', () => {
beforeEach(async () => {
await redis.flushdb();
});
it('expires keys after TTL', async () => {
await cache.set('key', 'value', { ttl: 1 });
expect(await cache.get('key')).toBe('value');
await sleep(1100);
expect(await cache.get('key')).toBeNull();
});
});
API Testing
// GOOD: Test against real services
describe('POST /users (integration)', () => {
it('creates user and caches result', async () => {
const response = await request(app)
.post('/users')
.send({ email: 'new@example.com' });
expect(response.status).toBe(201);
// Verify in real database
const user = await db('users').where({ email: 'new@example.com' }).first();
expect(user).toBeDefined();
// Verify in real cache
const cached = await redis.get(`user:${user.id}`);
expect(cached).toBeDefined();
});
});
Troubleshooting
| Problem | Solution |
|---------|----------|
| Service not starting | Check docker-compose logs [service] |
| Connection refused | Ensure service is running and port is correct |
| Database doesn't exist | Run migrations: pnpm migrate |
| Tests pass locally, fail in CI | Environment variable mismatch—check .env vs CI config |
| Flaky integration tests | Check for proper test isolation and cleanup |
Checklist
Before creating PR:
- [ ] Identified all service dependencies for changes
- [ ] All required services are running locally
- [ ] Integration tests pass against real services
- [ ] Posted local testing artifact to issue
- [ ] Did not rely on mocks for service-dependent code
Integration
This skill is called by:
tdd-full-coverage- For integration testing requirementsissue-driven-development- Before PR creationverification-before-merge- As a merge gate
This skill is enforced by:
validate-local-testing.sh- PreToolUse hook blocks PR without artifact
This skill references:
environment-bootstrap- For service startup patternssession-start- For service detection at session start