Cómo añadir pruebas unitarias y de integración en código PHP heredado sin afectar la producción
Por Kevin Marcondes · Publicado el · Actualizado el · 9 min de lectura
Necesitas saber cómo añadir pruebas unitarias y de integración en código PHP heredado sin poner en riesgo producción. Esta guía presenta un enfoque seguro e incremental: desde el diagnóstico hasta la automatización. Verás cómo ganar confianza en el legado, aislar dependencias y crear una base de pruebas que previene regresiones sin provocar interrupciones indeseadas en el entorno productivo.
Mapea el legado y los riesgos antes del primer test
Empieza por identificar dónde el código toca recursos externos. Base de datos, archivos y servicios HTTP son fuentes comunes de efectos secundarios. Lista los puntos de entrada, jobs y comandos cron que se ejecutan en producción.
Levanta dependencias y versiones. Revisa composer.json, extensiones de PHP e integraciones. Detectar versiones antiguas evita invertir tiempo en soluciones incompatibles.
Identifica acoplamientos fuertes. Singletons, funciones globales y llamadas estáticas difíciles de simular exigen “seams” de prueba. Un seam es un punto donde puedes inyectar comportamiento alternativo en tests.
Define un alcance mínimo viable. Tu primer objetivo es ejecutar un test sin tocar producción. Restringe el objetivo a módulos de bajo riesgo. A lo largo del proceso, considera soporte de mantenimiento continuo, como el mantenimiento de software.
Estrategia por capas: desde unitarios hasta smoke de integración
Usa una pirámide de pruebas. Muchos tests unitarios rápidos, algunos de integración fiables y pocos tests end-to-end. En legado, la pirámide reduce costos y acelera el feedback.
Comienza con unitarios en las reglas de negocio, luego integración en puntos críticos. Finalmente, los smoke tests validan el flujo esencial sin cubrir todos los escenarios.
La tabla resume cómo cada tipo ayuda en el legado:
| Tipo | Alcance | Velocidad | Aislamiento | Cuándo usar |
|---|---|---|---|---|
| Unitario | Funciones/métodos aislados | Muy alta | Alta | Reglas de negocio y cálculos sin I/O |
| Integración | Módulos con DB/HTTP/FS | Media | Media | Persistencia, repositorios, gateways |
| Contrato/API | Interfaces entre servicios | Media | Media | Garantizar formatos y compatibilidad |
| Smoke (E2E ligero) | Flujos esenciales | Baja | Baja | Verificar que lo básico funciona |
Entorno seguro: ramas, CI y datos de prueba controlados
Trabaja en una rama dedicada. Configura integración continua (CI) para ejecutar tests en cada commit. El objetivo es fallar pronto, lejos de producción.
Separa configuraciones por entorno. Usa archivos .env y un perfil específico de pruebas (.env.testing). Nunca uses credenciales reales en los tests.
Para la base de datos, prefiere una instancia aislada. Usa transacciones con rollback para mantener el estado limpio entre tests. Consulta la documentación de transacciones en PDO.
Crea datos determinísticos con fixtures o factories. Esto hace los tests reproducibles. En aplicaciones Laravel, las pruebas de base de datos se facilitan por el framework; consulta Pruebas de base de datos. Además, para gestión de pruebas como producto, revisa el servicio de software de prueba.
- Activa logs detallados en el entorno de pruebas.
- Bloquea salidas de e-mail y SMS al testear.
- Usa variables de entorno para endpoints falsos.
- Ejecuta tests en contenedores o VMs descartables.
Aislando dependencias difíciles en el código legado
Crea seams para romper acoplamientos. Encapsula llamadas estáticas y funciones globales en adaptadores. Inyecta esos adaptadores en las clases que quieras probar.
Para llamadas de tiempo y I/O, usa wrappers. En lugar de time() directamente, inyecta un Clock. En vez de file_get_contents, inyecta un FileReader. Esto permite usar mocks en los tests.
Usa mocks y stubs para simular dependencias. En PHPUnit, crea dobles para interfaces y clases inyectadas. Los dobles permiten enfocarse en la lógica, no en el mundo externo.
Cuando hay singletons, introduce una fábrica inyectable. Mantén la API pública, pero delega la creación a un proveedor configurable en los tests.
- Prioriza puntos con mayor acoplamiento y mayor riesgo.
- Introduce interfaces mínimas, sin grandes refactorizaciones.
- Extrae funciones puras a módulos separados.
- Evita cambiar la semántica del código en refactors iniciales.
Primeros tests unitarios: empieza pequeño y genera confianza
Elige unidades sin efectos secundarios. Reglas de negocio y validaciones son buenos candidatos. Empieza por funciones con entradas claras y salidas previsibles.
Crea tests de caracterización donde la lógica es oscura. Congela el comportamiento actual en tests. Después, refactoriza con seguridad manteniendo la especificación viva.
Convierte bugs en tests. Cada corrección debe ganar un caso que falle antes y pase después. Esto protege contra regresiones futuras.
- Mapea 5 funciones puras y escribe casos felices y de borde.
- Usa datos pequeños y legibles en los asserts.
- Evita múltiples escenarios en un solo test.
- Nombra los tests según el comportamiento esperado.
Tests de integración: DB, HTTP y archivos sin tocar producción
Para la base de datos, ejecuta en una instancia separada. Usa transacciones y rollback por test. En algunos casos, usar SQLite en memoria acelera ciclos, si es compatible con el ORM.
Para HTTP, sustituye clientes reales por handlers falsos. En Guzzle, un mock handler simula respuestas. Las integraciones externas reales deberían ejecutarse en staging, nunca en producción.
Para archivos, prefiere directorios temporales y wrappers. Streams como php://memory y php://temp evitan escribir en disco real. Revisa los wrappers soportados en stream wrappers.
Controla el orden de migraciones y seeds. Recrea el esquema antes de la suite y aísla estados con transacciones. Si necesitas apoyo táctico, consulta nuestra consultoría de software.
- Separa credenciales y endpoints de prueba.
- Usa timeouts cortos para fallos rápidos.
- Marca tests lentos y ejecútalos en jobs dedicados.
- Programa smoke tests tras deploy en staging.
Automatización y calidad continua en el legado
Define políticas de CI. Todo código nuevo debe incluir tests. ¿Cambiaste lógica? Añade o ajusta casos. La rotura de un test impide el merge hasta su corrección.
Incluye reportes útiles, sin obsesionarse con el porcentaje. Busca cobertura por riesgo, no un número absoluto. Prioriza módulos críticos para el negocio.
Implementa hooks locales. Un pre-commit ejecuta linters y la suite rápida de tests. El pipeline completo corre en el servidor de CI, con cache para acelerar.
Documenta cómo ejecutar tests localmente y en CI. Enseña cómo sembrar datos y cómo depurar fallos. Esto reduce fricción entre equipos y aumenta la adopción.
- Gates por rutas críticas del deploy.
- Smoke en staging tras migraciones.
- Alertas en el chat por fallos en main.
- Checklist de rollback por feature.
Camino incremental y cuándo buscar ayuda externa
Aplica una regla simple: nuevo código entra solo si está testeado. Código legado modificado gana caracterización antes de refactorizar. Así, la red de seguridad crece de forma orgánica.
Revisa periódicamente cuellos de botella y tests frágiles. Los flakes deben estabilizarse o eliminarse. Mantén tests rápidos y útiles para el equipo.
Si el equipo no tiene capacidad o experiencia, considera apoyo externo enfocado en pruebas de legado. La externalización puede acelerar los resultados sin detener el roadmap. Revisa opciones de tercerización o consultoría según convenga.
Para proyectos con gran superficie y múltiples integraciones, vale orquestar el esfuerzo dentro de un plan mayor de desarrollo de software empresarial.
Preguntas frecuentes
¿Necesito refactorizar antes de crear tests en código legado?
No es obligatorio. Empieza por tests de caracterización que congelan el comportamiento actual para detectar cambios sin cambiar la lógica. Introduce seams y pequeñas extracciones seguras (adaptadores, wrappers) para aislar dependencias; así podrás añadir mocks y stubs. A medida que crezca la red de tests, será posible realizar refactors mayores con menor riesgo porque los tests protegerán la semántica.
¿Cómo evitar que los tests accedan a producción por error?
Separa configuraciones por entorno usando archivos .env y un perfil específico (.env.testing), y usa credenciales y endpoints exclusivos de prueba. Bloquea envíos reales de e-mails y SMS, ejecuta la suite en contenedores o VMs descartables y usa transacciones con rollback por test para mantener la base de datos limpia. Complementa con CI en ramas dedicadas para fallar pronto fuera de producción.
¿PHPUnit o Pest funcionan bien con legado?
Sí: ambos runners funcionan con código legado. Lo esencial no es el runner sino poder aislar dependencias mediante seams, mocks, stubs y tests de caracterización. En frameworks como Laravel ambos se integran sin problema. Prioriza empezar a generar tests fiables y reutilizables; la elección entre PHPUnit o Pest puede definirse después según preferencia del equipo.
¿Qué probar primero en un sistema PHP legado?
Mapea primero el legado y los riesgos, y elige unidades con entradas y salidas claras: funciones puras, reglas de negocio y validaciones. Cubre módulos de alto valor para el negocio y bajo riesgo, y después puntos críticos de integración como repositorios y gateways. Convierte errores corregidos en tests de regresión para que la cobertura crezca alineada con el riesgo real.
¿Cuánto tiempo lleva ver beneficios de los tests?
Depende del estado del código y del alcance elegido; no hay un plazo único. Normalmente, tras cubrir los primeros módulos clave y convertir errores frecuentes en tests, el equipo nota menos regresiones y más confianza al cambiar código. Mantener la disciplina de añadir tests a cambios nuevos consolida y amplía esos beneficios con el tiempo.
¿Listo para probar tu PHP heredado con seguridad?
Si quieres implementar o acelerar la cobertura de pruebas en un sistema PHP heredado, hablemos sobre la mejor ruta para tu caso.
Hablar sobre pruebas en PHP heredadoVer también
- Guía: checklist de pre‑deploy y plan de rollback rápido para aplicaciones PHP en producción
- Checklist de seguridad para pipelines CI/CD con runners self‑hosted y gestión de secretos
- Guía: centralización de logs con Fluentd y Elasticsearch para aplicaciones PHP en VPS
- Guía: organizar migraciones y rollbacks en PostgreSQL para equipos pequeños