Estrategia de testing y mejores prácticas
Elige los tipos de test adecuados, fija objetivos de cobertura que importen, construye un pipeline de CI y elimina los tests inestables.
Busca en todas las páginas de la documentación
Elige los tipos de test adecuados, fija objetivos de cobertura que importen, construye un pipeline de CI y elimina los tests inestables.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
Testing Trophy (modelo de Kent C. Dodds para apps React):
┌──────────┐
│ E2E │ Pocos flujos críticos
┌─┴──────────┴─┐
│ Integration │ La mayoría de tests aquí
┌─┴──────────────┴─┐
│ Unit Tests │ Funciones de utilidad, hooks
┌─┴──────────────────┴─┐
│ Static Analysis │ TypeScript, ESLint
└──────────────────────┘
# Test strategy decision matrix
What to test:
- User-visible behavior (renders, interactions, navigation)
- Business logic (calculations, validation, state transitions)
- Edge cases (empty states, errors, loading, boundary values)
- Regression bugs (write a test for every bug fix)
What NOT to test:
- Implementation details (internal state, private methods)
- Third-party library internals
- CSS styling (unless visual regression matters)
- Trivial code (simple prop passthrough components)Cuándo usarlo: Antes de escribir cualquier test - planifica tu estrategia de testing según el tamaño del proyecto, el equipo y la tolerancia al riesgo.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Stage 1: Fast checks (run on every push)
lint-and-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
# Stage 2: Unit and integration tests (run on every push)
unit-tests:
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run test:run -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
# Stage 3: E2E tests (run on PRs to main and merges)
e2e-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Build application
run: npm run build
- name: Run E2E tests
run: npx playwright test --project=chromium
env:
CI: true
- name: Upload test results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
# Stage 4: Deploy (only on main)
deploy:
runs-on: ubuntu-latest
needs: [unit-tests, e2e-tests]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: echo "Deploy step here"// scripts de package.json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}Qué demuestra esto:
| Tipo de test | Velocidad | Confianza | Usar para |
|---|---|---|---|
| TypeScript | instantáneo | detecta errores de tipos | Todo el código |
| Unitario | rápido (menos de 10 ms cada uno) | corrección de la lógica | Funciones puras, hooks, utilidades, stores de Zustand |
| Integración | medio (menos de 100 ms cada uno) | comportamiento del componente | Formularios, listas, componentes interactivos |
| E2E | lento (segundos cada uno) | el sistema completo funciona | Flujos de login, checkout, rutas críticas |
// vitest.config.ts -- practical coverage thresholds
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
thresholds: {
// Set meaningful thresholds, not 100%
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.test.{ts,tsx}",
"src/**/*.stories.{ts,tsx}",
"src/**/*.d.ts",
"src/app/**/layout.tsx",
"src/app/**/loading.tsx",
"src/app/**/not-found.tsx",
],
},
},
});Cuándo la cobertura miente:
// Pattern: describe(unit) > it(behavior)
describe("CartStore", () => {
it("adds item to empty cart");
it("increments quantity for duplicate items");
it("removes item by id");
it("calculates total with mixed quantities");
});
// Pattern: should/when for complex behaviors
describe("CheckoutForm", () => {
it("submits order when all fields are valid");
it("shows validation error when email is missing");
it("disables submit button while processing");
});
// Avoid:
it("works"); // too vague
it("test addItem"); // do not start with "test"
it("should call setCount with count + 1"); // tests implementation| Síntoma | Causa | Solución |
|---|---|---|
| El test pasa en local y falla en CI | Diferencias de timing, variables de entorno faltantes | Usa waitFor en lugar de pausas fijas; revisa la config de entorno |
| El test falla de forma intermitente | Condiciones de carrera en código asíncrono | Añade aserciones adecuadas con reintento automático |
| El test falla solo en la primera ejecución | State de un test anterior que se filtra | Reinicia el state en beforeEach; usa contextos de navegador aislados |
| El test falla con "element not found" | El elemento aún no se ha renderizado | Usa consultas findBy o waitFor |
| Falla un test de captura de pantalla | Diferencias de renderizado de fuentes entre SO | Ejecuta tests visuales en Docker o limita a una plataforma |
CI mínima para proyectos pequeños:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run test:runHooks de pre-commit para feedback rápido:
// package.json (con lint-staged)
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "vitest related --run"]
}
}// TypeScript IS a testing tool -- strict mode catches bugs before tests
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
// These TypeScript errors replace entire categories of tests:
// - null/undefined access tests (strictNullChecks)
// - wrong argument type tests (strict function types)
// - missing property tests (exact optional property types)Probar detalles de implementación - Aserciones sobre state interno, clases CSS o conteos de llamadas a mocks acoplan los tests a la estructura del código. Solución: Prueba lo que el usuario ve y hace. Si el usuario no puede observarlo, el test probablemente no debería asertarlo.
Demasiados tests E2E - Los tests E2E son lentos, caros de mantener y propensos a inestabilidad. Solución: Cubre rutas críticas (login, checkout, registro) con E2E. Usa tests de integración para todo lo demás.
Perseguir el 100 % de cobertura - Los equipos gastan un esfuerzo desproporcionado probando código trivial para alcanzar objetivos arbitrarios. Solución: Fija umbrales de cobertura al 75-85 %. Enfoca el esfuerzo manual en probar código crítico para el negocio.
No ejecutar tests en CI - Los tests que solo corren en local acaban rompiéndose sin que nadie lo note. Solución: Configura CI desde el primer día. Bloquea merges ante fallos de tests.
Ignorar tests inestables - Marcar tests inestables como skip oculta bugs reales. Solución: Corrige la causa raíz (normalmente timing o filtrado de state). Pon en cuarentena los tests inestables en un job separado si hace falta.
Probar bibliotecas de terceros - Escribir tests que verifican que tu biblioteca de UI renderiza un desplegable correctamente desperdicia tiempo. Solución: Confía en que las bibliotecas están probadas. Prueba tu uso de ellas (¿pasas los props correctos, manejas los callbacks?).
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| Testing Trophy | Construyes apps React con mucha integración de componentes | Construyes una biblioteca de utilidades (entonces dominan los unitarios) |
| Testing Pyramid | Tienes una app tradicional con backend pesado y UI delgada | Tu app es mayormente frontend con llamadas a API |
| Sin E2E, solo integración | Proyecto pequeño, iteración rápida, sin flujos críticos de pago | Manejas dinero, autenticación o datos sensibles |
| Contract testing (Pact) | Microservicios con muchos equipos y consumidores de API | Monolito o un solo equipo |
Fija umbrales al 75-85 % para líneas, funciones y ramas. No persigas el 100 % - la cobertura mide qué líneas se ejecutaron, no qué aserciones son significativas.
No. Un test que renderiza un componente y no aserta nada sigue aumentando la cobertura. La cobertura mide ejecución de líneas, no verificación de comportamiento. Céntrate en probar rutas críticas y casos límite.
Usa describe(unidad) > it(comportamiento):
describe("CartStore", () => {
it("adds item to empty cart");
it("increments quantity for duplicate items");
});Evita nombres vagos como "works" o nombres de detalles de implementación.
waitFor en lugar de pausas fijas.beforeEach.findBy.skip sin corregir la causa raíz.Aserciones sobre state interno, clases CSS o conteos de llamadas a mocks acoplan los tests a la estructura del código. Cuando refactorizas, los tests se rompen aunque el comportamiento no haya cambiado. Prueba lo que el usuario ve en su lugar.
Con strict: true, TypeScript detecta:
Falla rápido - ejecuta primero las comprobaciones baratas.
Sí. Escribir un test de regresión por cada bug garantiza que no vuelva a aparecer. Es una de las prácticas de testing de mayor valor.
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "vitest related --run"]
}
}Esto ejecuta solo los tests relacionados con los archivos modificados antes de cada commit.
Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥