Guía de Estrategia de Testing de Software
Una guía práctica para construir una estrategia de testing en capas con tests unitarios, de integración, de contrato y end-to-end que tu equipo realmente mantendrá.
Introducción
La mayoría de los equipos no fallan por falta de tests — fallan porque sus tests no responden las preguntas que importan. Una suite de 3.000 tests que tarda 40 minutos y en la que nadie confía es peor que 300 tests que corren en 3 minutos y atrapan regresiones reales.
Una estrategia de testing no es un checklist. Es un conjunto de decisiones sobre dónde invertir tu esfuerzo limitado de testing para que los bugs se detecten temprano, los refactors sean seguros y la suite siga siendo lo suficientemente rápida como para que la gente la ejecute. Esta guía cubre cómo tomar esas decisiones: qué capa de la pirámide usar, qué metas de coverage tienen sentido, cómo empezar a testear código legacy y cómo conectar todo al CI sin frenar a los desarrolladores.
Está escrita para engineers y tech leads en equipos de uno a cincuenta personas. No hace falta conocimiento previo de frameworks de testing, aunque los ejemplos usan pytest y Jest porque son los más comunes. Si ya tienes una suite que funciona, puedes saltar a las secciones de metas de coverage e integración con CI — las primeras partes enmarcan decisiones que ya habrás tomado.
La Pirámide de Testing
La pirámide de tests distribuye los tests entre tres capas:
| Capa | Alcance | Velocidad | Costo | Cantidad |
|---|---|---|---|---|
| Unit | Función/clase individual | Milisegundos | Bajo | Muchos (70-80%) |
| Integration | Múltiples componentes | Segundos | Medio | Algunos (15-25%) |
| E2E | Flujos de usuario completos | Minutos | Alto | Pocos (5-10%) |
La pirámide existe por un trade-off simple: cuanto más cerca está un test de la infraestructura real, mejor atrapa fallos realistas — y más lento, frágil y caro de mantener se vuelve. Un bug detectado por un unit test cuesta milisegundos en detectarse y segundos en diagnosticarse. El mismo bug detectado por un E2E test cuesta minutos y a menudo horas de debugging a través de capas de infraestructura.
Para las proporciones en profundidad — el testing trophy, el anti-patrón ice cream cone y cómo migrar una suite invertida — mirá el patrón Test Pyramid.
Cómo Elegir Qué Testear
La pirámide te da las proporciones, pero no te dice a qué capa pertenece cada test. Esa decisión depende de tres factores:
¿Qué protege el test? Los cálculos de pagos, los flujos de autenticación y las migraciones de datos merecen tests en cada capa porque el costo de un bug es alto. Una función de utilidad que formatea una fecha necesita un unit test como máximo.
¿Dónde vive realmente el comportamiento? Testea el comportamiento en la capa más baja donde pueda verificarse. La lógica de validación vive en la función que valida — testeala como unidad, no a través de un request HTTP. Si te encuentras escribiendo tests E2E para verificar que el cálculo de un descuento redondea bien, el test está en la capa equivocada.
¿Cuánto cuesta el fallo vs el test? Un E2E test cuesta entre 100 y 1000 veces más que un unit test en escribir, correr y mantener. Gastá el presupuesto de E2E solo en journeys que dolerían al negocio si se rompieran — login, checkout, signup — no en caminos de código que un unit test ya cubre.
Una heurística útil en caso de duda: escribe el test en la capa más baja que todavía te dé confianza en que el código funciona. La confianza es la verdadera moneda. Si un unit test más un pequeño integration test te dan la misma confianza que un E2E, el E2E es desperdicio.
Unit Testing
Los unit tests verifican funciones o clases individuales en aislamiento. Son tu primera línea de defensa y deberían constituir el grueso de la suite.
Qué testear
- Lógica de negocio y algoritmos
- Casos edge (null, vacío, overflow)
- Paths de manejo de errores
- Condiciones de borde
Qué NO testear
- Código de framework (ORM, capa HTTP)
- Librerías de terceros
- Getters/setters simples sin lógica
Ejemplo (Python con pytest)
def calculate_discount(price: float, customer_type: str) -> float:
if customer_type == "vip":
return price * 0.8
return price * 0.95
import pytest
@pytest.mark.parametrize("price,customer_type,expected", [
(100.0, "vip", 80.0),
(100.0, "regular", 95.0),
(0.0, "vip", 0.0),
])
def test_calculate_discount(price, customer_type, expected):
assert calculate_discount(price, customer_type) == expected
Mantén cada unit test por debajo de un segundo. Si un “unit test” tarda 500ms, probablemente es un integration test disfrazado — toca el sistema de archivos, el reloj o un grafo de objetos real que no debería. Los unit tests lentos erosionan el feedback loop que hace valiosa a toda la suite.
Integration Testing
Los integration tests verifican que las partes de una feature funcionen juntas correctamente — y lo que es más importante, que cada boundary se ajuste a lo que ambos lados esperan.
Qué testear
- Queries de base de datos y migraciones
- Comportamiento de endpoints de API (con DB real o de test)
- Publicación/consumo de message queues
- Interacciones con servicios externos (con test doubles)
Ejemplo (Node.js con supertest)
const request = require('supertest');
const app = require('../app');
describe('POST /api/users', () => {
it('creates a user and returns 201', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' });
expect(res.status).toBe(201);
expect(res.body.email).toBe('alice@example.com');
});
});
El error más común en integration testing es hacer mock de la base de datos. Un fake en memoria nunca iguala la semántica real de las queries — índices, transacciones, constraints y coerción de tipos difieren todos. Usa Testcontainers o un equivalente para levantar una base de datos real por suite. Cuesta unos segundos de arranque y elimina toda una categoría de fallos tipo “en mi máquina funciona”.
Contract Testing
El contract testing es una capa intermedia que la mayoría de los equipos se salta, y es la razón por la que sus tests de integración entre microservicios son frágiles. En lugar de testear el sistema completo de punta a punta, testeas que las expectativas del consumidor sobre el proveedor siguen siendo ciertas.
Dos enfoques comunes:
- Contratos estilo Pact: el consumidor publica las formas de request/response que espera; el proveedor corre tests de verificación contra ellas. Ningún lado necesita al otro corriendo.
- Replay con VCR/cassettes: graba las respuestas reales de la API una vez, reprodúcelas en tests. Bueno para APIs de terceros (Stripe, GitHub) donde no puedes correr el proveedor.
Los contract tests atrapan el modo de fallo clásico donde el proveedor cambia el nombre de un campo y todos los tests con mock del consumidor siguen en verde. Para bases de datos y message queues, usa Testcontainers en su lugar — el propio esquema es el contrato.
Evita los mocks escritos a mano de servicios de otros equipos siempre que puedas. Codifican tu suposición de la API, no la API, y permanecen en verde mientras la realidad se desvía.
End-to-End Testing
Los E2E tests simulan el comportamiento de un usuario real a través de toda la aplicación. Son los tests más caros que escribirás — presupuéstalos en consecuencia.
Qué testear
- Journeys críticos de usuario (login, checkout, signup)
- Compatibilidad cross-browser
- Responsive mobile
- Cumplimiento de accesibilidad
Herramientas por stack
| Stack | Herramienta recomendada |
|---|---|
| Web | Playwright, Cypress |
| Mobile | Appium, Maestro |
| API | REST Assured, Postman |
Mantén los E2E tests determinísticos: waits explícitos en lugar de sleep(), datos de test sembrados en lugar de fixtures compartidos, y sin dependencias entre tests. Un E2E test flaky no solo desperdicia tiempo de CI — entrena al equipo para ignorar los fallos, lo que envenena la suite.
Metas de Coverage
- Line coverage: 70-80% mínimo para lógica de negocio. Consulta unit testing.
- Branch coverage: priorízalo sobre el line coverage — una línea puede ejecutarse mientras la mitad de sus ramas nunca corren.
- Critical paths: 100% coverage para flujos de payment, auth y seguridad.
El coverage es una métrica proxy, no el objetivo. Un codebase puede llegar al 90% de coverage y seguir enviando código roto — porque los tests ejecutan las líneas sin afirmar nada significativo sobre el comportamiento. Cuando el coverage y los bugs de producción no coinciden, los tests no están midiendo lo que importa.
Tres señales más fuertes para trackear junto al coverage:
- Mutation score: herramientas como
mutmuto Stryker introducen bugs pequeños y verifican si tus tests los atrapan. Un mutation score del 70% es más honesto que 90% de line coverage. - Tasa de bugs que se escapan: ¿cuántos bugs llegan a producción que un test podría haber atrapado? Este es el número que realmente responde “¿está funcionando nuestra estrategia?”.
- Duración de la suite: si la suite supera los ~10 minutos, los desarrolladores dejan de correrla en local y empiezan a confiar menos en el CI. La velocidad es un insumo de la estrategia, no una métrica de vanidad.
Testing en Código Legacy
Introducir una estrategia en un codebase sin tests se siente imposible porque lo es — no puedes escribir tests para código diseñado sin seams. El orden de operaciones importa:
- Agrega characterization tests alrededor de lo que vas a cambiar después. No apuntes a coverage — apunta a una red de seguridad sobre el código que vas a modificar. Estos tests describen el comportamiento actual, no el correcto; está bien.
- Escribe los tests antes del refactor, no después. Refactorizar para hacer el código testeable sin tests es cómo se estancan las migraciones de legacy. Primero fija el comportamiento.
- Prioriza por frecuencia de cambio, no por tamaño de archivo. El archivo de 500 líneas que cambia cada sprint necesita tests antes que el de 5.000 líneas que nadie toca.
- Introduce seams en los boundaries. La inyección de dependencias en los bordes (pasa el cliente HTTP, pasa el reloj, pasa el repositorio) hace testeable la mayoría del código legacy sin reescrituras.
Espera que los primeros tests sean feos — setups largos, clases concretas, fixtures reales. Es normal. Refactoriza los propios tests una vez que exista la red de seguridad, no antes.
Integración CI/CD
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:unit -- --coverage
- run: npm run test:integration
- run: npm run test:e2e
Tres reglas para una integración con CI que realmente se sostiene:
- Falla en regresión, no en targets. Bloquea un PR cuando el coverage baja respecto al commit anterior — no cuando está por debajo de un 80% global que alguien fijó hace dos años. Los gates de regresión se imponen solos.
- Cuarentena visible para los tests flaky. Un test flaky que sigue corriendo erosiona la confianza en silencio. Etiquétalo, muévelo a un job de cuarentena que no bloquea merges, y abre un ticket con responsable y fecha límite.
- Paraleliza antes de optimizar. Dividir los tests entre 4 workers suele valer más que micro-optimizar tests individuales. La mayoría de las suites vuelven a bajar de 10 minutos solo con paralelización.
Una versión lista para adaptar de este workflow — más el ejemplo pytest ejecutable — está en el repositorio compañero.
Estrategia de Testing por Tamaño de Equipo
La estrategia correcta cambia con la escala:
| Contexto | Estrategia |
|---|---|
| Solo / side project | Unit tests para la lógica central + 3-5 smoke tests E2E en caminos críticos. Omite contract tests. |
| Equipo pequeño (2-8) | Pirámide completa + gate en CI. Contract tests solo cuando corras servicios separados. |
| Organización | Pirámide + contract tests + infraestructura de tests dedicada. Dashboards de coverage por servicio, SLAs de cuarentena para flaky. |
Subinvertir es obvio. Sobreinvertir es más sutil: una startup de tres personas con contract testing y mutation gates está gastando horas de ingeniería en proceso que el code review detectaría más rápido. Ajusta el rigor al blast radius de un bug.
Dónde Encaja el Testing Manual
Una estrategia que solo describe automatización está incompleta. El exploratory testing — un humano buscando deliberadamente casos edge que la suite no cubre — encuentra bugs distintos a los de cualquier capa automatizada. Los tests escritos verifican lo que predijiste; las sesiones exploratorias sacan a la luz lo que no predijiste.
Tiene su lugar en tres momentos:
- Antes de automatizar una feature nueva. Explórala a mano primero. Los bugs que encuentres manualmente te dicen qué tests automatizados van a pagarse solos.
- En cada release de un flujo crítico. Diez minutos de una persona recorriendo el checkout atrapa regresiones de UX que ninguna assertion cubre — una etiqueta mal puesta, un layout roto, un estado vacío confuso.
- Cuando la suite sigue perdiéndose un tipo de bug. Escapes de producción repetidos en la misma zona significan que la red automatizada tiene un agujero; explora esa zona hasta entender por qué.
El error a evitar es usar el testing manual como sustituto de la automatización — los chequeos repetitivos de regresión pertenecen a las máquinas. El esfuerzo manual va donde va el criterio.
Lo que Funciona
- Escribe los tests primero (TDD) para lógica compleja o fixes de bugs — un test fallido que reproduce el bug previene regresiones para siempre.
- Usa test data builders en lugar de hardcodear fixtures; los tests se leen como el escenario que verifican.
- Haz mock de servicios externos en los boundaries de los integration tests, no dentro de la lógica de negocio.
- Corre los tests en paralelo para mantener los feedback loops por debajo de 10 minutos.
- Falla CI en regresión de coverage, no en targets arbitrarios.
- Mantén los E2E tests determinísticos: waits explícitos, datos sembrados, sin dependencias entre tests.
Errores Comunes
- Testear detalles de implementación en lugar de comportamiento — cada refactor rompe tests que deberían haber pasado.
- Compartir estado mutable entre tests — fallos dependientes del orden que solo aparecen en CI.
- Usar
sleep()en lugar de waits explícitos en E2E tests — la fuente número uno de flakiness. - Hacer mock de todo en integration tests, incluida la base de datos — los tests no verifican nada real.
- Ignorar los tests flaky en lugar de arreglarlos o ponerlos en cuarentena — toda la suite pierde credibilidad.
- Tratar la estrategia como un setup de una vez. La pirámide se erosiona: el código nuevo llega sin tests, los tests lentos se acumulan, y dos años después nadie confía en CI. Asigna a alguien que revise la salud de la suite trimestralmente.
- Copiar metas de coverage de otro equipo. 80% en un servicio de pagos y 80% en un dashboard interno no son la misma vara.
Troubleshooting
- Tests flaky: aísla estado compartido, tiempo y aleatoriedad. Haz los tests independientes y determinísticos; pon en cuarentena los persistentemente flaky.
- Coverage alto pero bugs en producción: el coverage no garantiza corrección. Agrega mutation testing, property-based tests o contract tests.
- Suite de tests lenta: paraleliza, haz mock de dependencias lentas y baja los E2E tests que verifican lógica a la capa de integración.
- Los tests pasan en local pero fallan en CI: revisa diferencias de entorno — timezone, locale, versiones de dependencias. Fija las versiones en lockfiles.
- Debugging de un integration test que falla: loguea los payloads de request/response, usa una base de datos de test dedicada y resetea el estado antes de cada test.
Ejemplo: Estrategia de Testing para Microservicios
Sistema: 8 microservicios, APIs REST + queue workers. Objetivo: 80% coverage, CI por debajo de 10 minutos, cero tests flaky.
| Nivel | Cantidad | Duración | Herramienta |
|---|---|---|---|
| Unit | 1200 | 2 min | Vitest + Jest |
| Integration | 300 | 4 min | Vitest + Testcontainers |
| Contract | 80 | 1 min | Pact |
| E2E | 40 | 3 min | Playwright |
| Total | 1620 | 10 min |
Tests por servicio:
| Servicio | Unit | Integration | Contract | E2E |
|---|---|---|---|---|
| auth-service | 200 | 40 | 10 | 5 |
| payment-service | 180 | 50 | 15 | 8 |
| user-service | 150 | 35 | 8 | 5 |
| order-service | 170 | 45 | 12 | 7 |
| notification-service | 120 | 30 | 5 | 3 |
| search-service | 130 | 25 | 8 | 4 |
| analytics-service | 100 | 20 | 5 | 2 |
| api-gateway | 150 | 55 | 17 | 6 |
Reglas que el equipo hace cumplir:
| Regla | Razón |
|---|---|
| Unit tests por debajo de 1s cada uno | Feedback rápido |
| Integration tests por debajo de 5s | Overhead de Testcontainers |
| E2E tests por debajo de 30s | Presupuesto de Playwright |
| Cero tests flaky | Re-run 3x; si falla una vez, se arregla |
| Coverage mínimo 70% en lógica de negocio | Gate en CI |
| Coverage mínimo 90% en payment-service | Camino crítico |
| PR bloqueado si baja el coverage | --coverage --check |
Pipeline de CI (10 min en total): lint + typecheck (1m) → unit (2m) → integration (4m) → contract (1m) → build (1m) → E2E en staging en paralelo con el deploy (3m).
Las lecciones se transfieren a sistemas más pequeños: mantén la forma de la pirámide, haz innegociables los contract tests entre servicios, trata un test flaky como peor que no tener test, y recuerda que el coverage es un proxy — el comportamiento es el objetivo.
Para convertir esto en un documento que todo el equipo pueda revisar y mantener, adapta la Plantilla de Estrategia de Pruebas.
Puntos Clave
- Distribuye los tests a lo largo de la pirámide y mantén la suite por debajo de ~10 minutos — la velocidad es lo que hace sobrevivible a la estrategia.
- Elige la capa del test según dónde vive el comportamiento y cuánto cuesta un fallo, no por costumbre.
- El coverage es un proxy; el mutation score y la tasa de bugs que se escapan son mejores señales de si la estrategia funciona.
- El código legacy se testea primero por frecuencia de cambio — red de seguridad antes del refactor.
- Un test flaky es peor que no tener test: cuarentena, responsable asignado, y se arregla o se borra.
Lectura Adicional
- Martin Fowler — The Practical Test Pyramid — la referencia canónica sobre la que se apoya esta guía.
- Google Testing Blog — Just Say No to More End-to-End Tests — por qué la pirámide se invierte y cuánto cuesta.
- Testcontainers — bases de datos y servicios reales en integration tests.
Preguntas frecuentes
Qué es la pirámide de testing?
La pirámide de testing es un modelo que sugiere tener muchos unit tests en la base, menos integration tests en el medio y muy pocos end-to-end tests en la cima. Esto mantiene la suite de tests rápida, confiable y económica.
Cuánto coverage de tests debería aspirar?
Aspira a 70-80% de coverage en la lógica de negocio crítica. Mayor coverage es mejor, pero el 100% no garantiza corrección. Enfócate en el comportamiento y los casos edge en lugar de porcentajes arbitrarios.
Debería hacer mock de APIs externas en integration tests?
Sí, haz mock de APIs externas en los boundaries de integration tests usando librerías como WireMock o MSW. Esto mantiene los tests determinísticos y rápidos mientras verifica los patrones de interacción de tu sistema.
Como manejo los tests de servicios externos?
Usa contract testing con Pact: el consumidor define las expectativas, el proveedor las verifica. Para APIs de terceros (Stripe, GitHub), graba las respuestas con VCR o cassettes. Para bases de datos, usa Testcontainers para levantar un container real por suite. Evita los mocks manuales — se mantienen en verde mientras la API real se desvía.
Cuándo debería escribir un E2E test en lugar de un integration test?
Escribe E2E solo cuando la cadena de valor cruza componentes que no controlas — autenticación con un identity provider, una redirección de pago real, un checkout crítico. Si cada componente del test es tuyo, un integration test atrapa los mismos bugs a una fracción del costo.
Recursos Relacionados
Pruebas Unitarias
Cómo escribir pruebas unitarias rápidas y deterministas con mocks y assertions en Python, JavaScript y Java.
GuideDesarrollo Guiado por Pruebas (TDD)
Aprende TDD paso a paso: escribe un test que falle, hazlo pasar, refactoriza. Red-Green-Refactor con ejemplos reales en Python, JavaScript y Java.
GuideTestcontainers: dependencias reales en tests de integración
Domina Testcontainers para pruebas de integración con bases de datos, message brokers y APIs reales. Cubre Java, Python y Node.js con fixtures basados en Docker.
DocPlantilla de Estrategia de Pruebas
Define tu enfoque de pruebas con esta plantilla de estrategia. Cubre alcance, tipos, herramientas, criterios de entrada/salida, riesgos y cronograma.
RecipeMedir Cobertura de Test
Cómo medir, reportar y hacer cumplir la cobertura de código con branch y condition coverage usando pytest-cov, nyc y JaCoCo para quality gates significativos.
GuideGuía de Pipelines CI/CD
Una guía práctica para construir pipelines CI/CD con GitHub Actions, testing, estrategias de deployment y procedimientos de rollback.