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 httpxEstructura
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 -vEjemplo: 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 -Dvitest.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 -- --watchEjemplo: 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@latestEjemplo
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.xmlBest Practices
Nombrar tests descriptivamente:
def test_create_user_with_invalid_email_returns_400():Arrange-Act-Assert:
def test_example(): # Arrange user = create_test_user() # Act result = service.process(user) # Assert assert result.status == "completed"Un assert por test (generalmente)
Mockearse dependencias externas:
@pytest.fixture def mock_external_api(mocker): return mocker.patch('app.services.external_api.call')