25/07/2005
Uno de los escenarios más comunes y frustrantes para un desarrollador de Java es ver cómo un código que funciona a la perfección en la aplicación activa, falla estrepitosamente con un temido NullPointerException durante la ejecución de pruebas unitarias. Este problema suele manifestarse en capas de servicio donde las dependencias, como los Mappers o Repositorios, no se inicializan correctamente en el contexto del test. Si alguna vez te has encontrado con que tu Mapper es nulo al ejecutar un test, no te preocupes, estás en el lugar correcto. A lo largo de este artículo, desglosaremos la causa raíz de este problema y te proporcionaremos una guía detallada y completa para testear correctamente tus servicios y sus mappers.

El objetivo de una prueba unitaria es verificar una pequeña pieza de código (una "unidad", como un método de un servicio) de forma aislada. Esto significa que debemos controlar todas sus dependencias externas para asegurarnos de que el resultado del test dependa únicamente de la lógica de la unidad que estamos probando, y no del comportamiento de sus colaboradores. Aquí es donde entran en juego los frameworks de mocking como Mockito.
¿Por Qué Mi Mapper es Nulo en el Test? El Origen del Problema
Para entender por qué tu customerMapper es nulo, primero debemos hablar de la Inyección de Dependencias (DI). En frameworks modernos como Spring, tú no creas las instancias de tus clases de servicio o repositorios manualmente. En su lugar, el contenedor de Spring (el Contexto de la Aplicación) se encarga de crear estos objetos (llamados Beans) y de "inyectar" las dependencias que necesitan. Cuando anotas un campo con @Autowired o lo declaras en un constructor, Spring busca un Bean compatible y lo asigna por ti.
Este mecanismo funciona de maravilla cuando la aplicación está en ejecución. Sin embargo, cuando ejecutas una prueba unitaria estándar (por ejemplo, con JUnit), el contexto completo de Spring no se levanta por defecto. La prueba se ejecuta en un entorno mucho más ligero y aislado. Como resultado, el mecanismo de inyección de dependencias de Spring no actúa, y cualquier campo que esperabas que fuera inyectado (como customerMapper y customerRepository) permanecerá con su valor por defecto, que para los objetos en Java es null.
Al llamar a this.customerMapper.map(...) sobre una referencia nula, se produce inevitablemente el NullPointerException que detiene tu prueba y te deja preguntándote qué ha salido mal.
La Solución Definitiva: Mocking con Mockito
La solución no es intentar levantar todo el contexto de Spring para una simple prueba unitaria (eso sería una prueba de integración, de la que hablaremos más adelante). La solución es tomar el control del entorno de prueba y proporcionar nosotros mismos las dependencias. Pero no necesitamos una implementación real; necesitamos un "doble de acción", un objeto simulado que se comporte exactamente como le digamos. A esto se le llama "mock".

Mockito es la librería de mocking más popular en el ecosistema Java. Nos permite crear objetos mock de cualquier clase o interfaz. Para el test de nuestro CustomerService, no nos interesa probar si el CustomerMapper mapea correctamente los objetos; eso debería tener su propia prueba unitaria. Lo que nos interesa es probar la lógica del método customerDetails. Por lo tanto, vamos a "mockear" tanto el repositorio como el mapper.
Configurando el Entorno de Prueba Paso a Paso
Veamos cómo transformar una prueba fallida en una prueba robusta y fiable. Usaremos JUnit 5 y Mockito.
Paso 1: Anotaciones Clave
Para que Mockito funcione con JUnit 5, necesitamos anotar nuestra clase de prueba con @ExtendWith(MockitoExtension.class). Esto inicializa los mocks y permite que la magia de la inyección de mocks funcione.
Paso 2: Declarando los Mocks y la Clase a Probar
A continuación, declaramos las dependencias que queremos mockear con la anotación @Mock. La clase que estamos probando, en este caso CustomerService, la anotamos con @InjectMocks. Esta anotación especial le dice a Mockito: "Crea una instancia real de esta clase e intenta inyectar en ella todos los campos anotados con @Mock que encuentres en esta clase de test".
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) class CustomerServiceTest { @Mock private CustomerRepository customerRepository; @Mock private CustomerMapper customerMapper; @InjectMocks private CustomerService customerService; // ... aquí irán nuestros métodos de test } Paso 3: Escribiendo el Método de Prueba (Arrange, Act, Assert)
Una buena práctica es estructurar los tests siguiendo el patrón AAA: Arrange (Preparar), Act (Actuar) y Assert (Verificar).
@Test void testCustomerDetails_cuandoClienteExiste_debeRetornarCliente() { // 1. Arrange (Preparar el escenario) // Creamos los objetos de datos que usaremos en el test int customerId = 1; Customer customerEntity = new Customer(); // Asumimos que tiene setters, o usamos un constructor customerEntity.setId(customerId); customerEntity.setName("John Doe"); CustomerDto customerDto = new CustomerDto(); customerDto.setId(customerId); customerDto.setFullName("John Doe"); // Configuramos el comportamiento de los mocks // Cuando se llame al repositorio, debe devolver nuestra entidad de prueba when(customerRepository.findCustomerByIdAndIsDeletedFalse(customerId)) .thenReturn(Optional.of(customerEntity)); // ¡EL PASO CLAVE! Cuando se llame al mapper, debe devolver nuestro DTO de prueba when(customerMapper.map(any(Customer.class))).thenReturn(customerDto); // 2. Act (Ejecutar el método que estamos probando) CustomerDto resultado = customerService.getById(customerId); // Probamos el método que contiene la lógica // 3. Assert (Verificar los resultados) assertNotNull(resultado, "El DTO no debería ser nulo"); assertEquals(customerId, resultado.getId(), "El ID del cliente no coincide"); assertEquals("John Doe", resultado.getFullName(), "El nombre del cliente no coincide"); // Opcionalmente, podemos verificar que los métodos de los mocks fueron llamados verify(customerRepository, times(1)).findCustomerByIdAndIsDeletedFalse(customerId); verify(customerMapper, times(1)).map(customerEntity); } En la sección "Arrange", definimos el comportamiento esperado de nuestras dependencias. La línea when(customerMapper.map(...)).thenReturn(...) es la solución directa al problema original. Le estamos diciendo a Mockito: "No me importa cómo funciona internamente el método map. Cuando sea invocado con cualquier objeto de tipo Customer, quiero que devuelvas este objeto customerDto que he preparado". Así, cuando la ejecución del código llega a la llamada al mapper, en lugar de ejecutar el código real sobre un objeto nulo, Mockito intercepta la llamada y devuelve el resultado que hemos predefinido.

Pruebas Unitarias vs. Pruebas de Integración
Es crucial entender la diferencia entre estos dos tipos de pruebas, ya que aborda la pregunta de "¿y si quiero probar el mapper real?".
| Característica | Prueba Unitaria (con Mocks) | Prueba de Integración |
|---|---|---|
| Propósito | Verificar una única unidad de código (un método) de forma aislada. | Verificar la interacción entre varios componentes (servicio, mapper, repositorio, base de datos). |
| Velocidad | Muy rápida. No necesita iniciar la aplicación ni acceder a BBDD. | Lenta. Requiere levantar el contexto de la aplicación. |
| Aislamiento | Alto. Un fallo indica un problema en la unidad probada. | Bajo. Un fallo puede deberse a cualquiera de los componentes integrados. |
| Dependencias | Se simulan (mockean). | Se usan las implementaciones reales (Beans de Spring). |
| Configuración | Simple. Anotaciones como @Mock y @InjectMocks. | Compleja. Anotaciones como @SpringBootTest, configuración de BBDD en memoria, etc. |
Preguntas Frecuentes (FAQ)
¿Por qué no usar la implementación real del Mapper en el test del servicio?
Porque el objetivo del test unitario del servicio es probar la lógica del *servicio*, no la del mapper. Si usas el mapper real y este falla, tu test de servicio fallará, pero la causa raíz no estará en el servicio. Cada componente debe tener sus propias pruebas unitarias para garantizar un aislamiento adecuado y facilitar la depuración.
¿Qué es @InjectMocks y en qué se diferencia de @Mock?
@Mock crea un objeto simulado (un doble) de una clase o interfaz. Este objeto no tiene lógica real; solo responde de la manera que tú configuras con when().thenReturn(). Por otro lado, @InjectMocks crea una instancia *real* de la clase que quieres probar, y luego intenta inyectar los mocks creados con @Mock en los campos correspondientes de esa instancia.
Mi Mapper (por ejemplo, con MapStruct) es una interfaz. ¿Cómo lo testeo?
MapStruct genera una implementación de tu interfaz en tiempo de compilación. Para probarla, puedes obtener una instancia real en tu clase de test de la siguiente manera:
private CustomerMapper mapper = Mappers.getMapper(CustomerMapper.class); Luego, en tus métodos de test, puedes llamar a sus métodos de mapeo con objetos de entrada y verificar que los objetos de salida tengan los valores correctos. Este sería el test unitario específico para el mapper.
Conclusión
Enfrentarse a un NullPointerException en las pruebas unitarias debido a una dependencia no inyectada es un rito de iniciación para muchos desarrolladores de Java. La clave para superarlo es comprender la diferencia fundamental entre el entorno de ejecución de la aplicación y el entorno aislado de una prueba unitaria. Al dominar el uso de herramientas como Mockito para simular dependencias, no solo solucionarás el problema del mapper nulo, sino que también escribirás pruebas más limpias, rápidas y mantenibles. Recuerda siempre el principio de aislamiento: una prueba unitaria debe probar una sola cosa. Al mockear las dependencias como el mapper, te aseguras de que tu test de servicio se centre exclusivamente en la lógica del servicio, que es exactamente su propósito.
Si quieres conocer otros artículos parecidos a Testing de Mappers en Java: Evita el NullPointer puedes visitar la categoría Juegos.
