Testing

Estrategia y estándares de testing

Cobertura Mínima

60% de cobertura de código es el mínimo aceptable para todos los proyectos.

Stack por Lenguaje

Lenguaje Unit Tests E2E Tests
Python pytest pytest + httpx
TypeScript Vitest Playwright

Python: pytest

Setup

pip install pytest pytest-asyncio pytest-cov httpx

Estructura

tests/
├── conftest.py          # Fixtures compartidos
├── test_main.py         # Tests de endpoints
├── test_services.py     # Tests de lógica
└── test_models.py       # Tests de modelos

Ejecutar Tests

# Todos los tests
pytest tests/ -v

# Con coverage
pytest tests/ -v --cov=app --cov-report=term-missing

# Un archivo específico
pytest tests/test_main.py -v

# Un test específico
pytest tests/test_main.py::test_healthz -v

Ejemplo: Test de Endpoint

import pytest
from httpx import AsyncClient
from app.main import app

@pytest.fixture
def anyio_backend():
    return 'asyncio'

@pytest.mark.anyio
async def test_healthz():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/healthz")
    assert response.status_code == 200
    assert response.json() == {"ok": True}

@pytest.mark.anyio
async def test_create_item():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.post(
            "/api/v1/items",
            json={"name": "Test Item", "price": 9.99}
        )
    assert response.status_code == 201
    assert response.json()["name"] == "Test Item"

Fixtures (conftest.py)

import pytest
from app.core.database import get_db
from app.main import app

@pytest.fixture
def test_db():
    """Base de datos de test"""
    # Setup
    yield db_session
    # Teardown
    db_session.rollback()

@pytest.fixture
def client(test_db):
    """Cliente HTTP con DB de test"""
    app.dependency_overrides[get_db] = lambda: test_db
    yield AsyncClient(app=app, base_url="http://test")
    app.dependency_overrides.clear()

TypeScript: Vitest

Setup

npm install vitest @testing-library/react @testing-library/jest-dom -D

vitest.config.ts

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      reporter: ['text', 'json', 'html'],
      threshold: {
        global: {
          lines: 60,
        },
      },
    },
  },
});

Ejecutar Tests

# Todos los tests
npm test

# Con coverage
npm test -- --coverage

# Watch mode
npm test -- --watch

Ejemplo: Test de Componente

import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';

describe('Counter', () => {
  it('renders initial count', () => {
    render(<Counter initialCount={5} />);
    expect(screen.getByText('Count: 5')).toBeInTheDocument();
  });

  it('increments on click', async () => {
    render(<Counter initialCount={0} />);
    fireEvent.click(screen.getByRole('button', { name: '+' }));
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
});

E2E: Playwright

Setup

npm init playwright@latest

Ejemplo

import { test, expect } from '@playwright/test';

test('homepage has title', async ({ page }) => {
  await page.goto('https://mi-proyecto.illanes00.cl/');
  await expect(page).toHaveTitle(/Mi Proyecto/);
});

test('login flow', async ({ page }) => {
  await page.goto('/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/dashboard');
});

CI Integration

GitHub Actions

# .github/workflows/ci.yml
- name: Test with pytest
  run: |
    pytest tests/ -v --cov=app --cov-report=xml

- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage.xml

Best Practices

  1. Nombrar tests descriptivamente:

    def test_create_user_with_invalid_email_returns_400():
  2. Arrange-Act-Assert:

    def test_example():
        # Arrange
        user = create_test_user()
    
        # Act
        result = service.process(user)
    
        # Assert
        assert result.status == "completed"
  3. Un assert por test (generalmente)

  4. Mockearse dependencias externas:

    @pytest.fixture
    def mock_external_api(mocker):
        return mocker.patch('app.services.external_api.call')