Testcontainers · Node.js 20 · Vitest · PostgreSQL 16 · Redis 7-alpine
Cada suite arranca su contenedor, aplica migraciones y lo destruye al finalizar — sin estado compartido entre suites.
// AgentFlow — UserRepository integration test import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql'; import { Pool } from 'pg'; import { UserRepository } from '../src/repos/userRepo'; import { runMigrations } from '../src/db/migrate'; import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; describe('UserRepository', () => { let container: StartedPostgreSqlContainer; let pool: Pool; let repo: UserRepository; beforeAll(async () => { container = await new PostgreSqlContainer('postgres:16') .withDatabase('agentflow_test') .withUsername('af_test') .withPassword('af_test') .start(); pool = new Pool({ connectionString: container.getConnectionUri() }); await runMigrations(pool); // aplica schema repo = new UserRepository(pool); }, 60_000); afterAll(async () => { await pool.end(); await container.stop(); }); afterEach(async () => { await pool.query('DELETE FROM users'); }); it('should create a user and return id', async () => { const user = await repo.create({ name: 'Ana García', email: 'ana@agentflow.io', role: 'admin', }); expect(user.id).toBeDefined(); expect(user.role).toBe('admin'); }); it('should enforce unique email → throws on duplicate', async () => { await repo.create({ name: 'Dupe', email: 'dup@test.io', role: 'viewer' }); await expect( repo.create({ name: 'Dupe2', email: 'dup@test.io', role: 'viewer' }) ).rejects.toThrow('unique_violation'); }); });
// AgentFlow — AgentRunRepository integration test import { PostgreSqlContainer } from '@testcontainers/postgresql'; import { AgentRunRepository } from '../src/repos/agentRunRepo'; import { runMigrations } from '../src/db/migrate'; describe('AgentRunRepository', () => { /* setup idéntico al de UserRepository */ it('should transition status: pending → running → done', async () => { const run = await repo.create({ projectId: 1, agentSlug: 'content-strategy', }); expect(run.status).toBe('pending'); await repo.updateStatus(run.id, 'running'); const running = await repo.findById(run.id); expect(running.status).toBe('running'); await repo.complete(run.id, { tokensUsed: 1_240, output: { headlines: ['Cómo la IA transforma…'] } }); const done = await repo.findById(run.id); expect(done.status).toBe('done'); expect(done.output.headlines).toHaveLength(1); }); it('should mark run as error with message', async () => { const run = await repo.create({ projectId: 1, agentSlug: 'copywriting', }); await repo.fail(run.id, 'LLM timeout after 30s'); const failed = await repo.findById(run.id); expect(failed.status).toBe('error'); expect(failed.errorMsg).toMatch(/timeout/); }); });
// AgentFlow — CacheService integration test (Redis) import { GenericContainer, StartedTestContainer } from 'testcontainers'; import { createClient, RedisClientType } from 'redis'; import { CacheService } from '../src/services/cache'; describe('CacheService', () => { let container: StartedTestContainer; let cache: CacheService; beforeAll(async () => { container = await new GenericContainer('redis:7-alpine') .withExposedPorts(6379) .start(); const redis = createClient({ url: `redis://${container.getHost()}:${container.getMappedPort(6379)}` }); await redis.connect(); cache = new CacheService(redis); }, 30_000); it('should cache and retrieve agent result', async () => { const result = { agentSlug: 'copywriting', output: { copy: 'Tu marca, tu historia.' } }; await cache.set('run:42', result, 300); const hit = await cache.get('run:42'); expect(hit).toEqual(result); }); it('should return null after TTL (1s)', async () => { await cache.set('run:tmp', { v: 1 }, 1); await new Promise(r => setTimeout(r, 1500)); expect(await cache.get('run:tmp')).toBeNull(); }); });
name: Integration Tests — AgentFlow on: push: branches: [main, dev] pull_request: jobs: integration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: npm - run: npm ci - name: Run integration tests run: npm run test:integration env: # Ryuk disabled en GitHub Actions TESTCONTAINERS_RYUK_DISABLED: "true" # Docker ya está disponible en ubuntu-latest
💡 Tip: Reutiliza el mismo contenedor entre tests de la misma suite con beforeAll + limpieza por afterEach DELETE. Con imagen cacheada localmente el primer arranque pasa de 8s a ~0.8s.