Una suite de pruebas es tan valiosa como el momento en que se ejecuta. Puedes tener las pruebas de Playwright más completas, la mejor configuración de contract testing y benchmarks de rendimiento meticulosos, pero si no se ejecutan en el momento correcto de tu pipeline de entrega, son documentación, no quality gates. El cuando y donde de la ejecución de pruebas es tan importante como el que.
Este artículo mapea el pipeline de testing completo, desde el momento en que un desarrollador guarda un archivo hasta que un cambio llega a producción. Compartire la arquitectura que uso en mis equipos, con configuraciones concretas de GitHub Actions que puedes adaptar a tus propios proyectos.
El Espectro del Pipeline de Testing
Piensa en tu pipeline de entrega como una serie de quality gates, cada uno con alcance y costo creciente:quality gates, each with increasing scope and cost:
- Pre-commit: se ejecuta en la máquina del desarrollador antes de hacer commit. Fracciones de segundo a segundos.
- PR / Pull Request: se ejecuta en CI cuando se abre o actualiza un PR. Minutos.
- Merge a main: se ejecuta después de que el código llega a la rama principal. Minutos a decenas de minutos.
- Despliegue: se ejecuta después del despliegue a un ambiente. Minutos.
- Producción: se ejecuta continuamente contra sistemas en vivo. Continuo.
Cada gate debe capturar diferentes categorías de defectos. El principio: fallar rápido, fallar barato. Captura lo que puedas localmente antes de que cueste minutos de CI, captura problemas de integración en checks de PR antes de que bloqueen al equipo, y válida el comportamiento en producción continuamente.
Pre-Commit: La Primera Puerta
Los hooks de pre-commit son el quality gate más barato que tienes. Se ejecutan en la máquina del desarrollador con cero costo de CI. El objetivo es capturar problemas obvios antes de que entren al repositorio.
Uso Husky con lint-staged para esto:
#!/usr/bin/env sh
# .husky/pre-commit
npx lint-staged {
"*.{ts,tsx}": [
"eslint --fix --max-warnings 0",
"prettier --write"
],
"*.{ts,tsx}": [
"tsc --noEmit --pretty"
],
"tests/unit/**/*.test.ts": [
"vitest run --reporter=dot"
]
} Esto asegura que cada commit tenga linting limpio, tipos correctos y tests unitarios pasando para los archivos modificados. Tiempo total de ejecución: menos de 10 segundos para la mayoría de los changesets. Los desarrolladores que se saltan este paso (usando --no-verify) deberían ser recordados gentilmente de que están intercambiando 10 segundos de retroalimentación local por 10 minutos de fallo en CI.
Etapa de PR: Integración y Regresión Visual
Cuando se abre un PR, el pipeline debe validar todo lo que el hook de pre-commit no cubre: pruebas de integración, regresión visual, escaneos de seguridad y verificaciones cross-browser. Aquí es donde debería ir la mayor parte de tu presupuesto de CI.
Este es el workflow de GitHub Actions que uso como línea base:
# .github/workflows/pr-checks.yml
name: PR Quality Gates
on:
pull_request:
branches: [main]
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
unit-and-lint:
runs-on: ubuntu-latest
timeout-minutes: 10
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:unit -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
integration:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/test_db
e2e:
runs-on: ubuntu-latest
timeout-minutes: 20
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
- run: npm run test:e2e -- --shard=${{ matrix.shard }}
strategy:
matrix:
shard: [1/3, 2/3, 3/3]
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-traces-${{ matrix.shard }}
path: test-results/
security:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high
- uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: CRITICAL,HIGH Varias decisiones de diseño son importantes aquí. El bloque concurrency cancela ejecuciones en progreso cuando un PR se actualiza, no tiene sentido ejecutar checks sobre un commit que el desarrollador ya supero. El job de E2E usa sharding para dividir la suite de Playwright en tres runners paralelos, reduciendo el tiempo de reloj en aproximadamente un 60%. Y la condición if: failure() en la subida de trazas asegura que solo almacenes artefactos de depuración cuando los tests realmente fallan, manteniendo bajos los costos de almacenamiento.
Merge a Main: Regresión Completa
Después de que se fusiona un PR, la rama principal debe ejecutar la suite de pruebas completa, incluyendo tests que son demasiado lentos o costosos para cada PR. Esto típicamente incluye:
- Suite E2E completa en todos los navegadores (Chromium, Firefox, WebKit)
- Benchmarks de rendimiento comparados contra línea base
- Verificación de contract tests contra todos los pacts de consumidores
- Regresión visual con comparación de capturas de pantalla
El pipeline de merge-to-main es tu red de seguridad. Los checks de PR pueden estar optimizados para velocidad (ejecutando solo Chromium, omitiendo tests lentos), pero el pipeline de merge ejecuta todo. Si algo se escapa, esta puerta lo captura antes del despliegue.
# Part of .github/workflows/merge-checks.yml
performance:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- run: npm run test:perf
- uses: actions/upload-artifact@v4
with:
name: perf-results
path: perf-results/
- name: Check performance budgets
run: |
node scripts/check-perf-budgets.js \
--baseline perf-results/baseline.json \
--current perf-results/current.json \
--threshold 10 Despliegue: Smoke Tests y Canarios
Después del despliegue a staging o producción, ejecuta smoke tests: un subconjunto pequeño y rápido de tu suite E2E que válida que las rutas críticas de usuario funcionan en el ambiente desplegado. Estos tests responden una pregunta: "es el despliegue lo suficientemente saludable para recibir tráfico?"
Para despliegues a producción, recomiendo una estrategia canario: enruta un pequeño porcentaje de tráfico (5-10%) a la nueva versión mientras ejecutas smoke tests. Si las pruebas pasan y las tasas de error se mantienen estables, incrementa gradualmente el tráfico. Si algo falla, revierte automáticamente.
// tests/smoke/critical-paths.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Smoke Tests - Critical Paths', () => {
test('homepage loads and displays key elements', async ({ page }) => {
await page.goto(process.env.DEPLOY_URL!);
await expect(page).toHaveTitle(/MyApp/);
await expect(page.getByRole('navigation')).toBeVisible();
});
test('user can authenticate', async ({ page }) => {
await page.goto(`${process.env.DEPLOY_URL}/login`);
await page.getByLabel('Email').fill(process.env.SMOKE_USER!);
await page.getByLabel('Password').fill(process.env.SMOKE_PASS!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Dashboard')).toBeVisible({ timeout: 10000 });
});
test('API health check returns OK', async ({ request }) => {
const response = await request.get(`${process.env.DEPLOY_URL}/api/health`);
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(body.status).toBe('healthy');
expect(body.database).toBe('connected');
});
}); Los smoke tests deben completarse en menos de 2 minutos. Si toman más, estás ejecutando demasiados. Sé implacable en mantener esta suite pequeña y rápida: su trabajo es detectar fallos catastróficos de despliegue, no capturar casos borde.
Estrategias de Paralelización
La velocidad del pipeline es una funcionalidad. Los pipelines lentos reducen la productividad de los desarrolladores, alientan a saltarse checks y aumentan el tamaño del lote de cambios, lo cual incrementa el riesgo. Estas son las estrategias de paralelización que aplico consistentemente:
- Paralelismo a nivel de job: Tests unitarios, de integración, E2E y escaneos de seguridad deben ejecutarse como jobs separados que inician simultáneamente. Un pipeline donde estos se ejecutan secuencialmente está desperdiciando el 60-70% de su tiempo de reloj.
- Test sharding: Divide suites de pruebas grandes en múltiples runners usando el flag
--shardintegrado de Playwright. Tres shards típicamente reducen la ejecución E2E de 15 minutos a 5. - Ejecución selectiva: Usa filtros de ruta para omitir jobs irrelevantes. Un cambio en documentación no debería activar tests E2E. El filtro
pathsde GitHub Actions lo hace sencillo. - Cache agresivo: Cachea
node_modules, navegadores de Playwright y capas de Docker. La diferencia entre un pipeline frío y uno caliente puede ser de 3-5 minutos.
Gestión de Artefactos
Cuando los tests fallan en CI, los desarrolladores necesitan suficiente contexto para depurar sin reproducir el fallo localmente. Esto significa recolectar y almacenar los artefactos correctos:
- Trazas de Playwright: Un archivo de traza contiene cada solicitud de red, snapshot del DOM y log de consola de una ejecución de test. Súbelos al fallar y los desarrolladores pueden depurar visualmente en el Trace Viewer.
- Capturas de pantalla y videos: Configura Playwright para capturar screenshots al fallar y videos para tests reintentados. Son invaluables para bugs visuales y fallos relacionados con timing.
- Reportes de tests: Genera reportes HTML que resuman conteos de pass/fail/skip, tiempo de ejecución por test y tendencias de fallos en el tiempo.
- Reportes de cobertura: Sube datos de cobertura y rastrea tendencias. Una cobertura en declive en una funcionalidad debería disparar una conversación, no un bloqueo automatizado.
Midiendo la Salud del Pipeline
Un pipeline es un sistema, y como cualquier sistema, necesita observabilidad. Estas son las métricas que rastreo:
- Duración P95 del pipeline: Cuánto tarda el percentil 95 de PRs en obtener retroalimentación? Objetivo: menos de 15 minutos para la suite completa de checks de PR.
- Tasa de flakiness: Qué porcentaje de fallos de tests son falsos positivos? Rastrealo semanalmente. Una tasa de flakiness superior al 5% erosiona la confianza en el pipeline. Los desarrolladores empiezan a ignorar fallos, y bugs reales se escapan.
- MTTR (Tiempo Medio de Reparación): Cuando el pipeline se rompe, cuánto tarda en estar verde de nuevo? Esto mide qué tan rápido el equipo responde a regresiones de calidad.
- Tiempo en cola: Cuánto esperan los jobs por un runner antes de ejecutarse? Si los tiempos en cola están consistentemente sobre 2 minutos, necesitas más runners o mejor gestión de concurrencia.
#!/bin/bash
# scripts/pipeline-metrics.sh — collect weekly pipeline health metrics
echo "=== Pipeline Health Report ==="
echo "Period: $(date -d '7 days ago' +%Y-%m-%d) to $(date +%Y-%m-%d)"
echo ""
# P95 duration (from GitHub API)
gh api repos/$REPO/actions/runs \
--jq '[.workflow_runs[] | select(.conclusion=="success") | .run_started_at as $start | .updated_at as $end | (($end | fromdate) - ($start | fromdate))] | sort | .[length * 0.95 | floor]' \
| xargs -I {} echo "P95 Duration: {} seconds"
# Flake rate (tests that failed then passed on retry)
echo "Flake Rate: $(cat test-results/flake-report.json | jq '.flakeRate')"
# Failed runs this week
gh api repos/$REPO/actions/runs \
--jq '[.workflow_runs[] | select(.conclusion=="failure")] | length' \
| xargs -I {} echo "Failed Runs: {}" Un pipeline rápido y confiable es una ventaja competitiva. Los equipos con ciclos de retroalimentación de 10 minutos despliegan 3 veces más seguido que los equipos con pipelines de 45 minutos, y con menos incidentes en producción.
Tu pipeline CI/CD es la columna vertebral de tu estrategia de calidad. Diseñalo con el mismo cuidado con que diseñarías la arquitectura de tu aplicación: separación clara de responsabilidades en cada etapa, ciclos de retroalimentación rápidos, recolección completa de artefactos y métricas de salud observables. El pipeline no solo debería ejecutar pruebas, debería darle a tu equipo confianza en que cada merge a main es seguro para desplegar.
Comentarios
0 comentariosTodos los comentarios son moderados y aparecerán después de la revisión.