EE Eugenio Estrada
Volver al blog

La ilusión de la cobertura de código: por qué tus pruebas unitarias no te salvan en producción

Testing Ingeniería de Software Arquitectura
Escuchar artículo
💡

Cobertura no equivale a confianza

  • La Ley de Goodhart en acción: Convertir la cobertura de código en un objetivo corporativo (ej. exigir un 90% obligatorio) incentiva pruebas de baja calidad y tests redundantes que no validan escenarios reales.
  • La trampa del acoplamiento: Abusar de dobles de prueba (mocks) para aislar unidades de código oculta errores críticos en las fronteras de integración y encadena tus pruebas a los detalles de implementación interna.
  • El enfoque físico del testing: El software real falla en los bordes (red, persistencia, concurrencia, serialización). Rediseñar tu estrategia combinando pruebas unitarias de dominio con pruebas de integración y contrato es la única vía para lograr un despliegue seguro.

En las reuniones de ingeniería de software es habitual escuchar la métrica de cobertura de código (code coverage) como el estándar de oro de la salud de un proyecto. Equipos enteros celebran alcanzar un 90% o un 100% de cobertura bajo la premisa de que un porcentaje alto actúa como un escudo impenetrable frente a los bugs en producción.

Sin embargo, en el mundo real de los sistemas complejos, esta métrica a menudo genera una falsa sensación de seguridad. Como sentenció el pionero de la computación Edsger Dijkstra: “Las pruebas de software pueden demostrar la presencia de errores, pero nunca su ausencia”. Obsesionarse con medir qué porcentaje de líneas de código ejecuta nuestra suite de pruebas, ignorando qué están validando realmente y cómo afecta esto al diseño del sistema, es uno de los mayores errores de estrategia técnica que un equipo senior puede cometer.


1. La Ley de Goodhart y la corrupción de la métrica

Cuando una métrica se convierte en un objetivo de negocio o de gestión, deja de ser una buena métrica. Este principio, conocido como la Ley de Goodhart, se manifiesta con total claridad en el desarrollo de software cuando los departamentos de tecnología o los líderes de equipo imponen umbrales mínimos de cobertura (por ejemplo, el clásico 80% o 90% para poder superar la fase de integración continua).

En el momento en que los desarrolladores son evaluados por el porcentaje de cobertura, el incentivo del sistema cambia:

  • Pruebas sin aserciones reales: Se escriben tests que recorren ramas enteras de código simplemente para que la herramienta de cobertura los marque en verde, pero se omiten aserciones robustas de comportamiento o se usan aserciones genéricas que siempre pasan.
  • Foco en el código fácil de probar: Se invierte tiempo desproporcionado en probar lógicas triviales (como getters, setters, mapeadores simples o controladores CRUD) mientras que se evitan los flujos asíncronos complejos, condiciones de carrera o integraciones de red complicadas debido a la dificultad que requiere su configuración.
  • Ruido y sobrecoste de mantenimiento: La suite de pruebas crece en volumen pero no en capacidad de detección de fallos, ralentizando el ciclo de desarrollo (feedback loop) y aumentando drásticamente la deuda técnica de mantenimiento de los propios tests.

2. El peligro de los ‘mocks’ y el acoplamiento al diseño interno

El paradigma del testeo unitario ortodoxo exige aislar por completo la unidad bajo prueba (usualmente una clase o función). Para conseguir esto en sistemas modernos, los ingenieros tienden a abusar de los dobles de prueba (mocks, stubs y spies).

Si bien los mocks son herramientas útiles para simular efectos colaterales costosos (como el envío de un correo electrónico o llamadas a pasarelas de pago de terceros), su uso indiscriminado para aislar cada capa arquitectónica (Service -> Repository -> ORM) introduce dos problemas de diseño fundamentales:

El acoplamiento a los detalles de implementación

Cuando mockeas los colaboradores internos de una clase, el test debe conocer obligatoriamente cómo interactúa esa clase con sus dependencias (qué métodos llama, en qué orden y con qué parámetros exactos).

  • Consecuencia: Si decides refactorizar el diseño interno de un módulo para mejorar su estructura —sin alterar su comportamiento externo—, todos tus tests unitarios se romperán. En lugar de facilitar la refactorización (que es el propósito principal que Martin Fowler defiende en su obra clásica Refactoring), los tests unitarios hiper-mockeados actúan como un molde de hormigón que penaliza cualquier cambio en la estructura del código.

La ilusión de la frontera verde

Las herramientas de cobertura marcan como “probado” el código que interactúa con un mock, pero el mock es una asunción que tú mismo has escrito.

  • El riesgo: Si el comportamiento del componente real cambia (por ejemplo, el ORM lanza una nueva excepción de base de datos bajo ciertas circunstancias, o un endpoint cambia el tipo de dato que devuelve), tu test unitario seguirá pasando en verde porque tu mock sigue respondiendo según la asunción antigua. El software real fallará estrepitosamente en producción a pesar de tener un 100% de cobertura en los informes locales.

3. La física del software: donde realmente rompen los sistemas

El software no suele fallar porque una función algorítmica pura sume mal dos números (algo que los tests unitarios detectan a la perfección). La física de los sistemas de producción modernos nos muestra que las caídas y los fallos críticos se producen en las fronteras y en el comportamiento dinámico del sistema:

  • Fallos de red y timeouts: ¿Cómo se comporta tu aplicación cuando la base de datos tarda más de 500ms en responder? ¿Manejas adecuadamente los reintentos con retroceso exponencial (exponential backoff) y disyuntores (circuit breakers)?
  • Consistencia y transacciones: ¿Qué ocurre si la escritura en la tabla A tiene éxito pero la actualización de la tabla B falla? Las pruebas unitarias con bases de datos mockeadas en memoria no pueden validar condiciones de aislamiento ni bloqueos transaccionales (locks).
  • Serialización e integridad de esquemas: Un cambio sutil en el nombre de un campo JSON en un microservicio de terceros provocará fallos de parseo si los tests de contrato o integración no están allí para detectarlo en el pipeline de despliegue.

Como explican Steve Freeman y Nat Pryce en su libro clásico Growing Object-Oriented Software, Guided by Tests, las pruebas deben dar soporte al diseño, ayudándonos a descubrir si las abstracciones elegidas son las correctas y si las conexiones entre ellas funcionan armónicamente bajo condiciones reales de carga e infraestructura.


4. Diseñando una estrategia de testing pragmática

Para salir de la ilusión de la cobertura y construir una red de seguridad real, los equipos deben reorientar su estrategia bajo los siguientes principios prácticos:

Evaluar la cobertura por su valor de diagnóstico, no de control

Usa las herramientas de cobertura para responder una única pregunta: “¿Qué partes del sistema están completamente ciegas de pruebas?”. La cobertura es útil para descubrir zonas del código desprotegidas, pero nunca debe usarse como una métrica de aprobación o éxito.

Priorizar pruebas sociables frente a pruebas solitarias

En lugar de forzar que cada test sea estrictamente unitario y solitario, permite el uso de tests sociables que prueben varios componentes juntos. Si estás probando la lógica de negocio, no mockees tu base de datos; utiliza herramientas como Testcontainers para levantar una base de datos real idéntica a la de producción (por ejemplo, PostgreSQL) en un contenedor Docker ligero durante la ejecución del test. Esto te dará un feedback 100% real sobre constraints, tipos de datos y queries N+1.

Introducir pruebas de contrato e integración

Para proteger las fronteras externas, implementa tests de integración y tests de contrato orientados al consumidor. Esto asegura que la comunicación entre tus servicios y proveedores externos de datos sea sólida y se mantenga consistente ante cambios de esquemas y despliegues independientes.

Validar con pruebas de aceptación (el verdadero valor de negocio)

Las pruebas de integración y de contrato blindan la conectividad técnica, pero son las pruebas de aceptación (a menudo estructuradas con lenguajes como Gherkin bajo metodologías BDD) las que validan que el sistema cumple el objetivo de negocio real. Un test de aceptación ejecuta la aplicación de punta a punta (end-to-end) o sobre un subsistema sociable amplio, verificando que un caso de uso real del usuario se complete satisfactoriamente. Si todos tus tests unitarios pasan en verde pero el test de aceptación falla, significa que el software está técnicamente construido pero funcionalmente inútil.


Conclusión: hacia la confianza de despliegue

Medir la salud de un proyecto basándose únicamente en el porcentaje de cobertura de código es el equivalente a evaluar la calidad de un avión contando los tornillos inspeccionados individualmente en el hangar, sin realizar jamás un vuelo de prueba.

El objetivo final de la ingeniería de software no es escribir pruebas de cobertura, sino obtener la confianza real de despliegue. Esto se logra aceptando que algunos módulos puros de lógica compleja requerirán un 100% de cobertura detallada (como motores de cálculo o políticas de negocio complejas), mientras que otras áreas del sistema se protegerán mejor mediante una red más amplia de pruebas de integración y aceptación que validen el flujo de valor real del usuario final de extremo a extremo.