Generación de Tests con OpenCode: De Cero a Cobertura Completa
Escribir tests manualmente es tedioso pero esencial. OpenCode puede generar tests automáticamente basándose en tu código existente, detectar edge cases que olvidarías y crear mocks realistas. El resultado: cobertura alta sin el dolor tradicional.
Este tutorial asume que dominas el flujo Plan/Build y las técnicas de debugging. Aquí nos enfocamos exclusivamente en generación de tests.
Generación de Tests Unitarios
Análisis de código existente
Para generar tests efectivos, OpenCode necesita entender el código:
[Plan Mode]
Analiza @src/services/UserService.ts y genera tests unitarios
completos que cubran:
1. Todos los métodos públicos
2. Casos de éxito
3. Casos de error
4. Edge cases (inputs vacíos, null, undefined)
5. Comportamiento asíncrono
Usa Vitest como framework. Sigue el patrón Given-When-Then.
OpenCode generará:
typescriptdescribe('UserService', () => { describe('findById', () => { it('should return user when found', async () => { // Given const userId = '123'; const expectedUser = { id: userId, name: 'John' }; mockRepository.findById.mockResolvedValue(expectedUser); // When const result = await userService.findById(userId); // Then expect(result).toEqual(expectedUser); }); it('should throw NotFoundError when user not found', async () => { // Given const userId = '999'; mockRepository.findById.mockResolvedValue(null); // When const result = userService.findById(userId); // Then await expect(result).rejects.toThrow(NotFoundError); }); it('should throw ValidationError for invalid ID', async () => { // Given const invalidId = ''; // When const result = userService.findById(invalidId); // Then await expect(result).rejects.toThrow(ValidationError); }); }); });
Tests para funciones puras
Funciones sin efectos secundarios son más fáciles de testear:
Genera tests para @src/utils/calculator.ts.
Esta función es pura, así que los tests pueden ser exhaustivos
sin necesidad de mocks.
typescriptdescribe('Calculator', () => { describe('calculateCompoundInterest', () => { it('should calculate correctly for standard inputs', () => { const result = calculateCompoundInterest(1000, 0.05, 12); expect(result).toBeCloseTo(1795.86, 2); }); it('should handle zero principal', () => { const result = calculateCompoundInterest(0, 0.05, 12); expect(result).toBe(0); }); it('should handle zero interest rate', () => { const result = calculateCompoundInterest(1000, 0, 12); expect(result).toBe(1000); }); it('should handle negative interest', () => { const result = calculateCompoundInterest(1000, -0.05, 12); expect(result).toBeLessThan(1000); }); }); });
Creación de Mocks
Mocking de dependencias externas
[Plan Mode]
@src/services/EmailService.ts depende de nodemailer.
Genera tests con mocks que:
1. Simulen envío exitoso
2. Simulen error de conexión
3. Verifiquen que se llamó con los parámetros correctos
4. No envíen emails reales en tests
OpenCode creará:
typescriptimport { vi } from 'vitest'; describe('EmailService', () => { const mockTransporter = { sendMail: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); }); it('should send email with correct parameters', async () => { // Given mockTransporter.sendMail.mockResolvedValue({ messageId: '123' }); const emailService = new EmailService(mockTransporter); // When await emailService.sendWelcome('user@example.com', 'John'); // Then expect(mockTransporter.sendMail).toHaveBeenCalledWith({ to: 'user@example.com', subject: 'Welcome', html: expect.stringContaining('John'), }); }); it('should handle send errors gracefully', async () => { // Given mockTransporter.sendMail.mockRejectedValue(new Error('SMTP error')); const emailService = new EmailService(mockTransporter); // When const result = emailService.sendWelcome('user@example.com', 'John'); // Then await expect(result).rejects.toThrow('Failed to send email'); }); });
Mocking de base de datos
Genera tests para @src/repositories/UserRepository.ts
usando mocks de Prisma Client.
Los mocks deben:
- Simular queries
- Retornar datos realistas
- Manejar errores de BD
- No requerir BD real
typescriptconst mockPrisma = { user: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn(), }, }; describe('UserRepository', () => { it('should find user by email', async () => { mockPrisma.user.findUnique.mockResolvedValue({ id: '1', email: 'test@example.com', }); const result = await userRepository.findByEmail('test@example.com'); expect(result).toEqual({ id: '1', email: 'test@example.com' }); expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ where: { email: 'test@example.com' }, }); }); });
Tests de Integración
Tests de API endpoints
[Plan Mode]
Genera tests de integración para @src/api/users.ts usando
supertest y Express app real.
Tests deben:
- Hacer requests HTTP reales
- Verificar status codes
- Validar responses
- Usar BD de test (no producción)
typescriptimport request from 'supertest'; import app from '@/app'; describe('POST /api/users', () => { it('should create user with valid data', async () => { const response = await request(app) .post('/api/users') .send({ name: 'John Doe', email: 'john@example.com', password: 'secure123', }) .expect(201); expect(response.body).toMatchObject({ id: expect.any(String), name: 'John Doe', email: 'john@example.com', }); expect(response.body).not.toHaveProperty('password'); }); it('should return 400 for invalid email', async () => { await request(app) .post('/api/users') .send({ name: 'John Doe', email: 'invalid-email', password: 'secure123', }) .expect(400); }); });
Tests de workflows complejos
Genera tests de integración para el flujo completo de checkout:
1. Crear usuario
2. Añadir items al carrito
3. Procesar pago
4. Verificar orden creada
5. Verificar email enviado
Usa BD de test y mocks de servicios externos.
Tests de Componentes React
Tests de componentes con Vitest
[Plan Mode]
Genera tests para @src/components/UserCard.tsx usando
@testing-library/react.
Tests deben:
- Renderizar correctamente con props
- Manejar eventos de usuario
- Verificar contenido renderizado
- No probar implementación interna
typescriptimport { render, screen, fireEvent } from '@testing-library/react'; import { UserCard } from '@/components/UserCard'; describe('UserCard', () => { const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com', }; it('should render user information', () => { render(<UserCard user={mockUser} />); expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); it('should call onDelete when delete button clicked', () => { const mockOnDelete = vi.fn(); render(<UserCard user={mockUser} onDelete={mockOnDelete} />); fireEvent.click(screen.getByRole('button', { name: /delete/i })); expect(mockOnDelete).toHaveBeenCalledWith('1'); }); });
Tests E2E con Playwright
Generación de tests E2E
[Plan Mode]
Genera tests E2E para el flujo de login usando Playwright.
Tests deben:
- Navegar a la página
- Llenar formularios
- Verificar redirecciones
- Verificar contenido después del login
- Usar datos de test
typescriptimport { test, expect } from '@playwright/test'; test('user can login with valid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]'); await expect(page).toHaveURL('/dashboard'); await expect(page.locator('h1')).toContainText('Welcome'); }); test('shows error for invalid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'wrong-password'); await page.click('button[type="submit"]'); await expect(page.locator('.error')).toContainText('Invalid credentials'); });
Estrategias de Cobertura
Análisis de cobertura actual
Analiza la cobertura de tests actual en el proyecto:
- Ejecuta npm run test:coverage
- Identifica archivos sin tests
- Identifica funciones no cubiertas
- Propone tests para las partes faltantes
Tests de edge cases
OpenCode detecta edge cases que podrías olvidar:
Genera tests de edge cases para @src/utils/validator.ts:
Considera:
- Strings vacíos
- null y undefined
- Valores límite (min/max)
- Tipos incorrectos
- Caracteres especiales
- Unicode y emojis
typescriptdescribe('Validator', () => { describe('validateEmail', () => { it('should accept valid email', () => { expect(validateEmail('test@example.com')).toBe(true); }); it('should reject empty string', () => { expect(validateEmail('')).toBe(false); }); it('should reject null', () => { expect(validateEmail(null)).toBe(false); }); it('should reject email without @', () => { expect(validateEmail('invalidemail.com')).toBe(false); }); it('should handle unicode emails', () => { expect(validateEmail('tëst@ëxämple.com')).toBe(true); }); }); });
Tests de Regresión
Generación desde bugs históricos
[Plan Mode]
Basándote en el issue #123 (bug de cálculo de impuestos),
genera un test que prevenga que este bug reaparezca.
El bug ocurría cuando:
- El orden tenía más de 10 items
- El usuario era de una región específica
- Se aplicaba un descuento
El test debe verificar que el cálculo sea correcto
en este escenario específico.
Tests de características críticas
Genera tests de regresión para las características más
críticas del sistema:
1. Autenticación y autorización
2. Procesamiento de pagos
3. Cálculos financieros
4. Exportación de datos
5. Integraciones con APIs externas
Cada test debe verificar el comportamiento exacto
documentado en los requisitos.
Workflows de Testing
Workflow 1: TDD con OpenCode
1. [Plan Mode] Describe la feature a implementar
2. Pide a OpenCode que genere tests primero
3. [Build Mode] Crea los tests (deben fallar)
4. Implementa el código mínimo para pasar tests
5. Refactoriza si necesario
6. Repite
Workflow 2: Tests para código existente
1. Selecciona un archivo sin tests
2. [Plan Mode] Pide generación de tests completos
3. Revisa los tests generados
4. [Build Mode] Crea los archivos de test
5. Ejecuta tests
6. Ajusta tests que no pasen (código vs tests)
7. Commitea tests y código
Workflow 3: Tests antes de refactor
1. Antes de refactorizar, genera tests de caracterización
2. Tests deben pasar con código actual
3. Ejecuta refactor
4. Tests deben seguir pasando
5. Si tests fallan: el refactor cambió comportamiento
Errores Comunes en Generación de Tests
| Error | Causa | Solución |
|---|---|---|
| Tests no pasan inmediatamente | Código tiene bugs | Corrige código o ajusta tests a comportamiento real |
| Tests son demasiado frágiles | Prueban implementación | Prueba comportamiento, no detalles internos |
| Mocks no realistas | Datos de test irreales | Usa datos que reflejen producción |
| Tests lentos | Mucha configuración en beforeEach | Usa factories y setup compartido |
| Coverage alta pero tests débiles | Tests triviales | Añade tests de edge cases y escenarios reales |
Mejores Prácticas
Nomenclatura de tests
Genera tests con nombres descriptivos que sigan el patrón:
"should [expected behavior] when [condition]"
Ejemplos:
- should return user when found
- should throw error when ID is invalid
- should calculate total including tax when user is VAT registered
Organización de tests
Organiza los tests generados siguiendo esta estructura:
tests/
├── unit/
│ ├── services/
│ ├── utils/
│ └── lib/
├── integration/
│ ├── api/
│ └── workflows/
└── e2e/
├── auth.spec.ts
└── checkout.spec.ts
Datos de test
Crea factories para datos de test consistentes:
[Plan Mode]
Genera factories en @tests/factories/ para:
- User
- Order
- Product
- Transaction
Las factories deben tener valores por defecto realistas
y permitir override de campos específicos.
typescript// @tests/factories/user.ts export const userFactory = (overrides = {}) => ({ id: '1', name: 'Test User', email: 'test@example.com', ...overrides, });
Conclusión
Generar tests con OpenCode requiere disciplina:
- Revisa siempre: Los tests generados necesitan revisión humana
- Tests de caracterización: Para código legacy, genera tests que describan comportamiento actual
- Edge cases: Pide explícitamente tests de casos límite
- Mocks realistas: Los mocks deben reflejar comportamiento real de dependencias
- Cobertura significativa: No persigas覆盖率 porcentual, persigue tests valiosos
En Binary Core, aumentamos nuestra cobertura de tests del 40% al 85% en 3 meses usando OpenCode para generación de tests. La clave no es generar todos los tests automáticamente, sino generar la base y luego refinarla manualmente.
¿Listo para mejorar la calidad del código? Aprende a usar OpenCode para code review automatizado.
Serie completa:
- Instalación y primeros pasos
- Flujo de trabajo Plan/Build
- Configuración avanzada
- Debugging
- Refactors
- Generación de tests (este artículo)
Binary Core
Equipo Binary Core