
Patrones y antipatrones en pruebas unitarias con JUnit y Mockito
Escribir pruebas unitarias no consiste 煤nicamente en verificar que el c贸digo funcione hoy, sino en garantizar que siga funcionando ma帽ana cuando otro desarrollador (o t煤 mismo) realice un cambio. Sin embargo, un c贸digo de prueba mal estructurado, fr谩gil y dif铆cil de leer puede convertirse en una carga mayor que el propio c贸digo de producci贸n.
Cuando los tests fallan constantemente por razones ajenas a la l贸gica de negocio, los equipos pierden confianza en ellos y, eventualmente, dejan de mantenerlos. En este art铆culo, analizaremos los antipatrones m谩s comunes al escribir pruebas en Java utilizando JUnit y Mockito, y aplicaremos patrones de dise帽o para transformarlos en tests limpios, mantenibles y confiables.
驴Qu茅 define a un Test Limpio?
Antes de corregir los errores, hay que entender el objetivo. Un test limpio suele adherirse a los principios FIRST:
- Fast (R谩pido): Deben ejecutarse en milisegundos. Si son lentos, los desarrolladores evitar谩n correrlos.
- Independent (Independiente): Un test no debe depender del resultado ni del estado de otro test.
- Repeatable (Repetible): Deben dar el mismo resultado en la m谩quina local, en el servidor de CI/CD y en cualquier entorno.
- Self-Validating (Autovalidado): El resultado debe ser un claro 茅xito (verde) o fracaso (rojo), sin requerir inspecci贸n manual de logs.
- Timely (Oportuno): Deben escribirse en el momento adecuado, idealmente junto con (o antes de) el c贸digo de producci贸n.
A continuaci贸n se plantean test con malos h谩bitos que rompen estas reglas y c贸mo solucionarlos.
Antipatr贸n 1: Test Gigante (The God Test / Giant Test) vs. Patr贸n: Arrange-Act-Assert (AAA)
El Problema
El God Test es una prueba monol铆tica de cientos de l铆neas que intenta verificar m煤ltiples escenarios a la vez. Mezcla la creaci贸n de datos, la ejecuci贸n y decenas de aserciones (assertEquals, assertTrue) sin ning煤n orden. Si este test falla, es casi imposible saber qu茅 funcionalidad espec铆fica se rompi贸 sin usar el depurador.
La Soluci贸n: Patr贸n: Arrange-Act-Assert (AAA)
El patr贸n Arrange-Act-Assert (Preparar-Actuar-Afirmar) divide cada prueba en tres bloques l贸gicos y visuales muy claros. Un test debe probar un solo comportamiento.
En el siguiente ejemplo vemos que el test eval煤a de forma sobrecargada el flujo completo de procesamiento de una orden, ya que en una sola prueba prepara los datos del usuario y la orden, ejecuta el servicio y valida m煤ltiples efectos secundarios simult谩neos: la actualizaci贸n del estado a "PROCESADA", el registro del sello de tiempo y la notificaci贸n al usuario. Por esto, al incluir m煤ltiples aserciones adicionales sobre distintos flujos, rompe el principio de responsabilidad 煤nica de un test unitario (Assertion Roulette), haciendo que verifique varios comportamientos independientes en lugar de centrarse en un solo caso de uso.
El Antipatr贸n en acci贸n:
Java
@Test
void testProcesarOrden() {
// Setup confuso mezclado con validaciones
Orden orden = new Orden(1L, "Laptop");
orden.setEstado("PENDIENTE");
Usuario user = new Usuario("juan@test.com");
orden.setUsuario(user);
OrdenService service = new OrdenService(new NotificacionMock());
service.procesar(orden);
assertEquals("PROCESADA", orden.getEstado());
assertNotNull(orden.getFechaProcesamiento());
assertTrue(user.isNotificado());
// ... 15 aserciones m谩s de diferentes flujos
}
En el siguiente c贸digo se muestra c贸mo el test verifica de forma espec铆fica que, al procesar una orden en estado "PENDIENTE", su estado cambie a "PROCESADA" y se env铆e una notificaci贸n al usuario; en el Arrange prepara el usuario, la orden y simula el servicio de notificaci贸n; en Act ejecuta el m茅todo de procesamiento; y en Assert verifica 煤nicamente que el estado sea "PROCESADA" y que la notificaci贸n se haya enviado.
Aplicando el Patr贸n AAA:
Java
@Test
@DisplayName("Debe cambiar el estado de la orden a procesada y notificar al usuario")
void shouldProcessOrderAndNotifyUser() {
// Arrange (Preparar)
Usuario usuario = new Usuario("juan@test.com");
Orden orden = new Orden(1L, "Laptop", usuario, "PENDIENTE");
NotificacionService notificacionMock = mock(NotificacionService.class);
OrdenService ordenService = new OrdenService(notificacionMock);
// Act (Actuar)
ordenService.procesar(orden);
// Assert (Afirmar)
assertEquals("PROCESADA", orden.getEstado());
verify(notificacionMock, times(1)).enviar(usuario);
}
Nota: Usar @DisplayName en JUnit 5 o nombres de m茅todos descriptivos mejora enormemente la legibilidad cuando el test falla en la consola.
Antipatr贸n 2: Fuga de Configuraci贸n (Configuration Leakage) vs Patr贸n: Aislamiento de Configuraci贸n
El Problema
Un escenario sumamente frustrante (muy com煤n en aplicaciones Spring Boot) ocurre cuando los tests pasan en tu m谩quina, pero fallan en el entorno de otro desarrollador o en el pipeline. A menudo, esto sucede porque las pruebas est谩n leyendo el archivo application.properties de la carpeta principal (src/main/resources). Si alguien cambia una URL de base de datos, un token o un timeout en producci贸n, los tests se rompen misteriosamente. Esto viola el principio Repeatable.
La Soluci贸n: Aislamiento con Test Resources
Para que las pruebas sean deterministas, deben tener su propio entorno aislado. En proyectos Java (Maven/Gradle), la carpeta src/test/resources tiene prioridad sobre src/main/resources durante la fase de testing.
Buenas pr谩cticas de implementaci贸n:
- Duplicaci贸n controlada: Crea un archivo application.properties o application.yml exclusivo en src/test/resources.
- Uso de perfiles: Utiliza perfiles espec铆ficos para pruebas mediante la anotaci贸n @ActiveProfiles("test").
Java
@SpringBootTest
@ActiveProfiles("test") // Forzar谩 a Spring a leer application-test.yml
class FacturacionServiceTest {
@Value("${api.external.timeout}")
private int timeout; // En test siempre ser谩 fijo, independientemente de producci贸n
@Test
void shouldCalculateInvoiceCorrectly() {
// L贸gica de prueba con configuraci贸n predecible
}
}
Con este patr贸n, te aseguras de que el entorno de pruebas sea inmutable frente a los ajustes operativos del c贸digo principal.
Antipatr贸n 3: Exceso de Mocks (Over-mocking / Mocking the World) vs. Patr贸n: Mocking Estrat茅gico (Boundary Mocking)
El Problema
Con herramientas tan potentes como Mockito, es tentador usar @Mock para absolutamente todo. El antipatr贸n Over-mocking ocurre cuando el desarrollador falsea no solo las dependencias externas, sino tambi茅n objetos de dominio (DTOs, Entidades) y l贸gica interna. El resultado es un test que "pasa" pero no prueba nada real; est谩 probando la configuraci贸n del propio mock.
La Soluci贸n: Mocking Estrat茅gico
El patr贸n dicta una regla clara: Mockea las dependencias, pero usa instancias reales para los datos.
Solo debes usar Mocks para aislar la clase bajo prueba de componentes externos lentos o vol谩tiles (bases de datos, APIs de terceros, colas de mensajes).
El Antipatr贸n:
Java
@Test
void calcularTotalEnDolares_antipatron() {
// MAL: Mockeando una estructura de datos simple
Pedido pedidoMock = mock(Pedido.class);
when(pedidoMock.getTotalLocal()).thenReturn(50000.0);
when(pedidoMock.getMonedaOriginal()).thenReturn("COP");
// BIEN: Mockear EXCLUSIVAMENTE dependencias externas o procesos pesados
ServicioDeCambio servicioCambioMock = mock(ServicioDeCambio.class);
when(servicioCambioMock.obtenerTasaDolar("COP")).thenReturn(0.00025);
ProcesadorDePagos procesador = new ProcesadorDePagos(servicioCambioMock);
double totalDolares = procesador.calcularTotalEnDolares(pedidoMock);
assertEquals(12.5, totalDolares);
}
El patr贸n:
Aqu铆 aplicamos la buena pr谩ctica. Instanciamos el Pedido real porque su estado es predecible y reside en memoria. Solo creamos un mock del ServicioDeCambio porque no queremos que nuestra prueba unitaria dependa de una conexi贸n a internet o de una API de terceros.
Java
@Test
void calcularTotalEnDolares_patron() {
// BIEN: Usar el objeto real para estructuras de datos y entidades
Pedido pedido = new Pedido(50000.0, "COP");
// BIEN: Mockear EXCLUSIVAMENTE dependencias externas o procesos pesados
ServicioDeCambio servicioCambioMock = mock(ServicioDeCambio.class);
when(servicioCambioMock.obtenerTasaDolar("COP")).thenReturn(0.00025);
ProcesadorDePagos procesador = new ProcesadorDePagos(servicioCambioMock);
double totalDolares = procesador.calcularTotalEnDolares(pedido);
assertEquals(12.5, totalDolares);
}
Antipatr贸n 4: Obsesi贸n por las M茅tricas (Coverage Obsession / Metric Tyranny) vs. Patr贸n: Supresi贸n Intencional (Intentional Suppression)
El Problema
Los linters y herramientas de an谩lisis est谩tico de c贸digo (como SonarQube o Checkstyle) son excelentes para mantener la calidad en src/main/java. Sin embargo, aplicar exactamente las mismas reglas inflexibles al c贸digo de pruebas suele generar problemas.
Por ejemplo, SonarQube podr铆a fallar el pipeline quej谩ndose de Magic Numbers (n煤meros quemados en el c贸digo) o cadenas de texto duplicadas en los asserts. Para evitar que el pipeline falle, los desarrolladores a veces caen en el antipatr贸n de colocar un desastroso @SuppressWarnings("all") en toda la clase de prueba, apagando por completo las validaciones.
La Soluci贸n: Supresi贸n Consciente
El c贸digo de prueba tiene est谩ndares diferentes al c贸digo de producci贸n. En un test, la duplicaci贸n de literales de texto (cadenas esperadas) o el uso de n煤meros espec铆ficos (IDs de prueba) a menudo mejora la legibilidad, porque permite ver el valor exacto sin tener que navegar a constantes en otros archivos.
El patr贸n consiste en entender los l铆mites y usar la supresi贸n de forma quir煤rgica.
Java
// Supresi贸n espec铆fica solo para lo que es aceptable en un contexto de prueba
@SuppressWarnings("java:S1192") // String literals should not be duplicated (SonarQube)
class UsuarioValidacionTest {
@Test
void shouldRejectEmptyEmail() {
Usuario user = new Usuario("");
Exception error = assertThrows(ValidationException.class, () -> service.validar(user));
// Duplicar "El email es requerido" aqu铆 es preferible a una constante gen茅rica porque hace el test expl铆cito y f谩cil de leer.
assertEquals("El email es requerido", error.getMessage());
}
}
Nota: Se debe silenciar de forma espec铆fica las alertas que restan claridad al test (como la visibilidad de paquetes, literales duplicados o magic numbers), pero prestar atenci贸n cuando la herramienta advierte sobre aserciones faltantes, excepciones ignoradas o una complejidad ciclom谩tica excesiva en la prueba.
Consideraciones Finales
Un error recurrente al hablar de pruebas unitarias es obsesionarse con la m茅trica de cobertura (Code Coverage). Herramientas como JaCoCo pueden indicarte que tienes un 100% de cobertura de l铆neas, pero eso solo significa que el c贸digo fue ejecutado durante la prueba, no que fue verificado. Un test con 100% de cobertura y sin ninguna aserci贸n (assert o verify) es un test in煤til.
Es mejor priorizar siempre la calidad y el dise帽o arquitect贸nico de la prueba sobre el simple cumplimiento de una m茅trica de porcentaje.
Conclusi贸n
El c贸digo de pruebas debe ser tratado como c贸digo de primera clase, exigiendo el mismo nivel de cuidado, refactorizaci贸n y dise帽o que el c贸digo destinado a producci贸n. La implementaci贸n de patrones como el AAA, la gesti贸n adecuada de los recursos de configuraci贸n, el uso responsable de mocks y el manejo estrat茅gico de las herramientas de an谩lisis est谩tico, permiten transformar las pruebas unitarias de un mero requisito administrativo a una red de seguridad verdaderamente confiable. En consecuencia, la inversi贸n de tiempo en la estructuraci贸n y limpieza de las pruebas constituye una pr谩ctica que beneficia profundamente a los equipos de desarrollo durante las futuras sesiones de depuraci贸n y resoluci贸n de incidentes.
Entradas anteriores

ETL vs ELT: Diferencias, Casos de Uso y Mejores Herramientas
驴ETL o ELT? Conoce sus diferencias, ventajas, mejores herramientas modernas y casos de uso pr谩cticos explicados de forma clara y sin complicaciones.

Fine-tuning para detecci贸n de fraude: cu谩ndo s铆 conviene
Gu铆a t茅cnica sobre fine-tuning para detecci贸n de fraude: cu谩ndo supera a entrenar de cero, impacto real y arquitectura MLOps en AWS.
