AgentSkillsCN

testing-integration

编写集成测试,验证各组件在真实依赖条件下能否协同运作、准确无误。

SKILL.md
--- frontmatter
name: testing-integration
description: "Write integration tests that verify multiple components work together correctly with real dependencies"

Skill: Testing Integration

Goal

Write integration tests that verify multiple components work together correctly with real dependencies like databases, caches, and external services.

Use This Skill When

  • Testing how multiple modules work together
  • Testing database operations with real queries
  • Testing API endpoints with request/response cycles
  • Testing cache + database coordination
  • The user asks for "integration tests"

Do Not Use This Skill When

  • Testing single functions in isolation (use unit tests)
  • Testing complete user journeys (use E2E tests)
  • Tests should run in milliseconds (prefer unit tests)
  • External services are unavailable (mock or skip)

Integration Test Structure

typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { UserService } from './user.service';
import { Database } from './database';
import { Cache } from './cache';

describe('UserService Integration', () => {
  let db: Database;
  let cache: Cache;
  let service: UserService;

  beforeAll(async () => {
    db = await Database.connect(process.env.TEST_DB_URL);
    cache = await Cache.connect(process.env.TEST_CACHE_URL);
    service = new UserService(db, cache);
  });

  afterAll(async () => {
    await db.cleanup();
    await cache.cleanup();
  });

  describe('user creation and retrieval', () => {
    it('creates a user and retrieves it from cache', async () => {
      // Create user
      const created = await service.createUser({
        email: 'test@example.com',
        name: 'Test User'
      });
      expect(created.id).toBeDefined();
      expect(created.email).toBe('test@example.com');

      // Retrieve (should hit cache)
      const retrieved = await service.getUser(created.id);
      expect(retrieved).toEqual(created);

      // Verify cache was hit (implementation detail)
      const cacheStats = await cache.getStats();
      expect(cacheStats.hits).toBeGreaterThan(0);
    });
  });
});

When to Use Integration Tests

ScenarioUse Integration TestReason
Database CRUDVerify queries work correctly
Cache + DB coordinationTest cache invalidation
API endpointsTest full request/response
Service-to-service callsTest contract and data flow
Single utility functionUse unit test instead
User login → purchase flowUse E2E test instead

Test Database Setup

In-Memory Database

typescript
import Database from 'better-sqlite3';
import { Kysely, SqliteDialect } from '@promethean-os/persistence';

export function createTestDatabase() {
  const db = new Database(':memory:');
  const dialect = new SqliteDialect({ database: db });
  const queryDb = new Kysely<any>({ dialect });
  
  // Initialize schema
  queryDb.schema
    .createTable('users')
    .addColumn('id', 'varchar(255)', col => col.primaryKey())
    .addColumn('email', 'varchar(255)', col => col.notNull())
    .addColumn('name', 'varchar(255)', col => col.notNull())
    .execute();
  
  return queryDb;
}

Docker/Test Containers

typescript
import { GenericContainer } from 'testcontainers';

let postgresContainer: GenericContainer;

beforeAll(async () => {
  postgresContainer = await GenericContainer
    .fromDockerfile('.')
    .withEnvironment({ POSTGRES_DB: 'test' })
    .withExposedPorts(5432)
    .start();
  
  process.env.TEST_DB_URL = postgresContainer.getConnectionUri();
});

afterAll(async () => {
  await postgresContainer.stop();
});

Testing Database Operations

typescript
describe('UserRepository', () => {
  let repo: UserRepository;
  
  beforeEach(async () => {
    repo = new UserRepository(testDb);
    await repo.clear(); // Clean state before each test
  });

  it('creates a user and persists to database', async () => {
    const user = await repo.create({
      email: 'new@example.com',
      name: 'New User'
    });

    expect(user.id).toBeDefined();

    // Verify in database directly
    const found = await testDb
      .selectFrom('users')
      .selectAll()
      .where('id', '=', user.id)
      .executeTakeFirst();

    expect(found.email).toBe('new@example.com');
  });

  it('finds users by email', async () => {
    await repo.create({ email: 'findme@example.com', name: 'Test' });
    
    const users = await repo.findByEmail('findme@example.com');
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe('Test');
  });

  it('updates user and reflects changes', async () => {
    const created = await repo.create({ email: 'old@example.com', name: 'Old' });
    
    const updated = await repo.update(created.id, { name: 'New' });
    expect(updated.name).toBe('New');

    // Verify persistence
    const found = await repo.findById(created.id);
    expect(found.name).toBe('New');
  });

  it('soft deletes user', async () => {
    const created = await repo.create({ email: 'delete@example.com', name: 'Delete' });
    
    await repo.softDelete(created.id);
    
    const found = await repo.findById(created.id);
    expect(found.deletedAt).toBeDefined();
  });
});

Testing Cache Integration

typescript
describe('Cache + Repository Integration', () => {
  let repo: UserRepository;
  let cache: Cache;

  beforeEach(async () => {
    await repo.clear();
    await cache.flush();
  });

  it('caches user after first read', async () => {
    const created = await repo.create({ email: 'cache@example.com', name: 'Test' });
    
    // First read - cache miss
    const user1 = await service.getUser(created.id);
    expect(user1).toEqual(created);

    // Verify cache was populated
    const cached = await cache.get(`user:${created.id}`);
    expect(cached).toBeDefined();

    // Second read - cache hit
    const user2 = await service.getUser(created.id);
    expect(user2).toEqual(user1);

    // Verify only one database query occurred
    const dbQueries = await testDb.getQueryCount();
    expect(dbQueries).toBe(1);
  });

  it('invalidates cache on update', async () => {
    const created = await repo.create({ email: 'update@example.com', name: 'Old' });
    
    // Populate cache
    await service.getUser(created.id);

    // Update user
    await service.updateUser(created.id, { name: 'New' });

    // Verify cache was invalidated
    const cached = await cache.get(`user:${created.id}`);
    expect(cached).toBeNull();
  });
});

Testing API Endpoints

typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestApp } from './test-utils';
import { createUser, getUser } from './handlers';

describe('User API Endpoints', () => {
  let app: TestApp;

  beforeAll(async () => {
    app = await createTestApp();
    app.route('POST /users', createUser);
    app.route('GET /users/:id', getUser);
  });

  afterAll(async () => {
    await app.cleanup();
  });

  describe('POST /users', () => {
    it('creates a user and returns 201', async () => {
      const response = await app.request('POST /users', {
        body: { email: 'api@example.com', name: 'API Test' }
      });

      expect(response.status).toBe(201);
      const body = await response.json();
      expect(body.id).toBeDefined();
      expect(body.email).toBe('api@example.com');
    });

    it('returns 400 for invalid input', async () => {
      const response = await app.request('POST /users', {
        body: { email: 'invalid-email' }
      });

      expect(response.status).toBe(400);
    });
  });

  describe('GET /users/:id', () => {
    it('returns user by id', async () => {
      // Create user first
      const created = await app.request('POST /users', {
        body: { email: 'get@example.com', name: 'Get Test' }
      });
      const { id } = await created.json();

      // Get user
      const response = await app.request(`GET /users/${id}`);
      expect(response.status).toBe(200);
      
      const user = await response.json();
      expect(user.id).toBe(id);
      expect(user.email).toBe('get@example.com');
    });

    it('returns 404 for non-existent user', async () => {
      const response = await app.request('GET /users/non-existent-id');
      expect(response.status).toBe(404);
    });
  });
});

Service-to-Service Integration

typescript
describe('Notification Service Integration', () => {
  let notificationService: NotificationService;
  let emailClient: TestEmailClient;
  let queue: TestQueue;

  beforeEach(() => {
    emailClient = new TestEmailClient();
    queue = new TestQueue();
    notificationService = new NotificationService(emailClient, queue);
  });

  it('sends email notification through queue', async () => {
    await notificationService.sendEmail({
      to: 'user@example.com',
      subject: 'Test',
      body: 'Hello'
    });

    // Verify message was queued
    const messages = await queue.getMessages('notifications');
    expect(messages).toHaveLength(1);
    expect(messages[0].type).toBe('send_email');
    expect(messages[0].payload.to).toBe('user@example.com');
  });
});

Best Practices

1. Use Real Implementations Where Possible

typescript
// GOOD - use real database, mock only external services
const repo = new UserRepository(realDatabase);
const emailService = mockEmailService();

// BAD - over-mock, test nothing meaningful
const repo = mockUserRepository();
expect(repo.create).toHaveBeenCalled();

2. Clean State Between Tests

typescript
beforeEach(async () => {
  await testDb.clearTables(['users', 'posts', 'comments']);
  await cache.flush();
});

3. Use Transactions for Isolation

typescript
it('handles concurrent updates', async () => {
  const user = await repo.create({ balance: 100 });
  
  // Run concurrent withdrawals
  await Promise.all([
    withdraw(user.id, 30),
    withdraw(user.id, 40),
    withdraw(user.id, 50)
  ]);

  // Verify final balance (should handle race condition)
  const final = await repo.getBalance(user.id);
  expect(final.balance).toBe(80); // 100 - 30 - 40 - 50 + (handled)
});

4. Test Failure Modes

typescript
it('rolls back transaction on error', async () => {
  const initialCount = await repo.count();
  
  await expect(
    repo.createWithFailure({ invalid: 'data' })
  ).rejects.toThrow();

  const finalCount = await repo.count();
  expect(finalCount).toBe(initialCount); // No change
});

Output

  • Integration test files for multi-component scenarios
  • Real database/cache configuration with cleanup
  • Proper test data setup and teardown
  • Edge case testing for component interactions
  • Failure mode and rollback testing

References