opencodetestingtestscalidadautomatización

Generación de Tests con OpenCode: De Cero a Cobertura Completa

Por Binary Core

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á:

typescript
describe('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.
typescript
describe('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á:

typescript
import { 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
typescript
const 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)
typescript
import 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
typescript
import { 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
typescript
import { 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
typescript
describe('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

ErrorCausaSolución
Tests no pasan inmediatamenteCódigo tiene bugsCorrige código o ajusta tests a comportamiento real
Tests son demasiado frágilesPrueban implementaciónPrueba comportamiento, no detalles internos
Mocks no realistasDatos de test irrealesUsa datos que reflejen producción
Tests lentosMucha configuración en beforeEachUsa factories y setup compartido
Coverage alta pero tests débilesTests trivialesAñ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:

  1. Revisa siempre: Los tests generados necesitan revisión humana
  2. Tests de caracterización: Para código legacy, genera tests que describan comportamiento actual
  3. Edge cases: Pide explícitamente tests de casos límite
  4. Mocks realistas: Los mocks deben reflejar comportamiento real de dependencias
  5. 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:

  1. Instalación y primeros pasos
  2. Flujo de trabajo Plan/Build
  3. Configuración avanzada
  4. Debugging
  5. Refactors
  6. Generación de tests (este artículo)

Binary Core

Equipo Binary Core

← Volver al blog