🧪 Unit Tests
Tests de funciones y componentes aislados.
Frameworks: Vitest, Jest, Mocha
Estrategias completas de testing con ClaudeKit
Aprende estrategias completas de testing automatizado con ClaudeKit, desde unit tests hasta E2E testing.
¿Por qué testing? Los tests previenen regresiones, documentan comportamiento, y dan confianza para refactorizar.
🧪 Unit Tests
Tests de funciones y componentes aislados.
Frameworks: Vitest, Jest, Mocha
🔗 Integration Tests
Tests de integración entre módulos.
Frameworks: Supertest, Testing Library
🌐 E2E Tests
Tests de flujos completos de usuario.
Frameworks: Playwright, Cypress
⚡ Performance Tests
Tests de carga y rendimiento.
Frameworks: k6, Artillery
graph TD A[E2E Tests - Pocos] --> B[Integration Tests - Algunos] B --> C[Unit Tests - Muchos]
style A fill:#ff6b6b style B fill:#ffd93d style C fill:#6bcf7fRegla de oro:
Tests de funciones y componentes aislados.
import { describe, it, expect } from 'vitest';import { add, multiply } from './math';
describe('Math Functions', () => { describe('add', () => { it('should add two positive numbers', () => { expect(add(2, 3)).toBe(5); });
it('should handle negative numbers', () => { expect(add(-2, 3)).toBe(1); });
it('should handle zero', () => { expect(add(0, 5)).toBe(5); }); });
describe('multiply', () => { it('should multiply two numbers', () => { expect(multiply(2, 3)).toBe(6); });
it('should multiply by zero', () => { expect(multiply(5, 0)).toBe(0); }); });});import { render, screen } from '@testing-library/react';import { Button } from './Button';
describe('Button Component', () => { it('renders with text', () => { render(<Button>Click me</Button>); expect(screen.getByText('Click me')).toBeInTheDocument(); });
it('calls onClick when clicked', () => { const handleClick = jest.fn(); render(<Button onClick={handleClick}>Click me</Button>);
screen.getByText('Click me').click(); expect(handleClick).toHaveBeenCalledTimes(1); });
it('is disabled when disabled prop is true', () => { render(<Button disabled>Click me</Button>); expect(screen.getByRole('button')).toBeDisabled(); });});import pytestfrom math_operations import add, multiply
class TestMathFunctions: def test_add_positive_numbers(self): assert add(2, 3) == 5
def test_add_negative_numbers(self): assert add(-2, 3) == 1
def test_multiply_numbers(self): assert multiply(2, 3) == 6
def test_multiply_by_zero(self): assert multiply(5, 0) == 0
@pytest.mark.parametrize("a,b,expected", [ (1, 2, 3), (0, 0, 0), (-1, 1, 0), ]) def test_add_various_cases(self, a, b, expected): assert add(a, b) == expectedTests de integración entre módulos, APIs y databases.
import request from 'supertest';import { app } from './app';
describe('API Endpoints', () => { describe('POST /api/users', () => { it('should create a new user', async () => { const response = await request(app) .post('/api/users') .send({ name: 'John Doe', email: 'john@example.com', password: 'password123' }) .expect(201);
expect(response.body).toHaveProperty('id'); expect(response.body.email).toBe('john@example.com'); });
it('should return 400 for invalid email', async () => { await request(app) .post('/api/users') .send({ name: 'John Doe', email: 'invalid-email', password: 'password123' }) .expect(400); }); });
describe('GET /api/users/:id', () => { it('should return user by id', async () => { const response = await request(app) .get('/api/users/1') .expect(200);
expect(response.body).toHaveProperty('name'); expect(response.body).toHaveProperty('email'); });
it('should return 404 for non-existent user', async () => { await request(app) .get('/api/users/999') .expect(404); }); });});import { render, screen, waitFor } from '@testing-library/react';import { server } from './mocks/server';import { UserProfile } from './UserProfile';
beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());
describe('UserProfile Integration', () => { it('loads and displays user data', async () => { render(<UserProfile userId="1" />);
await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); }); expect(screen.getByText('john@example.com')).toBeInTheDocument(); });
it('shows error when API fails', async () => { server.use( rest.get('/api/users/1', (req, res, ctx) => { return res(ctx.status(500)); }) );
render(<UserProfile userId="1" />);
await waitFor(() => { expect(screen.getByText(/error/i)).toBeInTheDocument(); }); });});Tests de flujos completos de usuario.
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => { test('should login successfully', async ({ page }) => { await page.goto('http://localhost:3000/login');
await page.fill('input[name="email"]', 'user@example.com'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]');
await expect(page).toHaveURL('http://localhost:3000/dashboard'); await expect(page.locator('h1')).toContainText('Welcome'); });
test('should show error for invalid credentials', async ({ page }) => { await page.goto('http://localhost:3000/login');
await page.fill('input[name="email"]', 'user@example.com'); await page.fill('input[name="password"]', 'wrongpassword'); await page.click('button[type="submit"]');
await expect(page.locator('.error')).toContainText('Invalid credentials'); });
test('should redirect to login when not authenticated', async ({ page }) => { await page.goto('http://localhost:3000/dashboard'); await expect(page).toHaveURL('http://localhost:3000/login'); });});describe('Authentication Flow', () => { beforeEach(() => { cy.visit('/login'); });
it('should login successfully', () => { cy.get('input[name="email"]').type('user@example.com'); cy.get('input[name="password"]').type('password123'); cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard'); cy.contains('h1', 'Welcome').should('be.visible'); });
it('should show error for invalid credentials', () => { cy.get('input[name="email"]').type('user@example.com'); cy.get('input[name="password"]').type('wrongpassword'); cy.get('button[type="submit"]').click();
cy.get('.error').should('contain', 'Invalid credentials'); });
it('should redirect to login when not authenticated', () => { cy.visit('/dashboard'); cy.url().should('include', '/login'); });});/testEl comando /test ejecuta el workflow completo de testing:
# Ejecutar todos los testsclaude test
# Tests específicosclaude test "authentication flow"
# Tests con coverageclaude test --coverage
# Tests en paraleloclaude test --parallel
# Tests con reporteclaude test --report# 1. Implementar featureclaude cook "Add user authentication"
# 2. Tester agent crea testsclaude test "authentication"
# 3. Verificar coverageclaude test --coverage
# 4. Si coverage < 80%, agregar más testsclaude cook "Add tests for edge cases in auth"
# 5. Re-testearclaude test "authentication" --coveragedescribe('User Service', () => { it('should create user with valid data', () => { // Arrange - Preparar const userData = { name: 'John Doe', email: 'john@example.com' }; const expectedUser = { id: 1, ...userData };
// Act - Ejecutar const result = userService.create(userData);
// Assert - Verificar expect(result).toEqual(expectedUser); });});// ❌ MAL - Tests dependientesdescribe('User Service', () => { let userId: number;
it('creates user', () => { const user = userService.create({ name: 'John' }); userId = user.id; // Depende del test anterior });
it('deletes user', () => { userService.delete(userId); // Usa variable del test anterior });});
// ✅ BIEN - Tests independientesdescribe('User Service', () => { it('creates user', () => { const user = userService.create({ name: 'John' }); expect(user).toHaveProperty('id'); });
it('deletes user', () => { const user = userService.create({ name: 'John' }); userService.delete(user.id); expect(userService.find(user.id)).toBeNull(); });});// ❌ MAL - Nombre vagoit('works', () => { expect(add(2, 2)).toBe(4);});
// ✅ BIEN - Nombre descriptivoit('should add two positive numbers correctly', () => { expect(add(2, 2)).toBe(4);});// ❌ MAL - Múltiples assertsit('validates user data', () => { const user = createUser(); expect(user.name).toBeDefined(); expect(user.email).toBeDefined(); expect(user.age).toBeGreaterThan(0);});
// ✅ BIEN - Un assert por testit('should have a name', () => { const user = createUser(); expect(user.name).toBeDefined();});
it('should have an email', () => { const user = createUser(); expect(user.email).toBeDefined();});
it('should be over 18', () => { const user = createUser(); expect(user.age).toBeGreaterThan(0);});// Mock de API externavi.mock('./api', () => ({ fetchUser: vi.fn()}));
import { fetchUser } from './api';
describe('UserProfile', () => { it('displays user data', async () => { const mockUser = { id: 1, name: 'John Doe' }; vi.mocked(fetchUser).mockResolvedValue(mockUser);
// Test code... });});# Ver coverage actualnpm run test:coverage
# Output:# -----------|---------|----------|---------|---------|-------------------# File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s# -----------|---------|----------|---------|---------|-------------------# All files | 85.5 | 78.2 | 92.3 | 86.1 |# utils.ts | 95.2 | 90.5 | 100 | 95.2 | 45# api.ts | 72.1 | 65.3 | 85.7 | 73.4 | 123-145# -----------|---------|----------|---------|---------|-------------------| Tipo de Proyecto | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
| CRÍTICO (Financiero, Salud) | > 95% | > 90% | > 95% | > 95% |
| ALTO (Core business) | > 85% | > 80% | > 85% | > 85% |
| MEDIO (General) | > 75% | > 70% | > 75% | > 75% |
| BAJO (Prototipos) | > 60% | > 50% | > 60% | > 60% |
name: Test Suite
on: push: branches: [main] pull_request: branches: [main]
jobs: test: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20.x' cache: 'npm'
- name: Install dependencies run: npm ci
- name: Run tests run: npm test
- name: Generate coverage run: npm run test:coverage
- name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info
- name: Comment PR with coverage uses: romeovs/lcov-reporter-action@v0.3.1 with: lcov-file: ./coverage/lcov.info github-token: ${{ secrets.GITHUB_TOKEN }}Problema: Tests que pasan a veces y fallan otras.
Soluciones:
it('should work independently', async () => { // Reset state before test await resetDatabase();
// Test code...});it('should wait for async operation', async () => { await waitFor(() => { expect(element).toBeVisible(); });});// ❌ MALawait sleep(1000);
// ✅ BIENawait waitFor(() => expect(result).toBeDefined());Problema: Suite de tests toma demasiado tiempo.
Soluciones:
vitest --parallelvi.mock('./database', () => ({ query: vi.fn().mockResolvedValue([])}));// Solo correr tests lentos en CIconst runSlowTests = process.env.CI === 'true';
describe('API Integration', () => { (runSlowTests ? describe : describe.skip)('Slow endpoints', () => { // Tests lentos... });});Explora las skills más utilizadas en producción.
¿Preguntas? Usa claude ask "tu pregunta sobre testing" en cualquier momento.