En el desarrollo de aplicaciones basadas en Spring Boot, la capa de servicio implementa la lógica de negocio y a menudo interactúa con recursos externos. Realizra pruebas unitarias en esta capa es crucial para validar el código de forma aislada. Cuando se depende de bases de datos o servicios externos, frameworks de mocking como Mockito permiten simular estas dependencias, enfocándose en la lógica bajo prueba sin configuraciones complejas.
Pruebas unitarias para la capa de servicio en Spring Boot
Consideremos un servicio que consulta el número total de registros en una tabla de base de datos. El servicio inyecta un mapper para acceder a los datos. A continuación, un ejemplo simplificado:
@Service
public class BugServiceImpl implements BugService {
@Autowired
private BugMapper bugMapper;
@Override
public int countBugs() {
return bugMapper.getTotalCount();
}
}
El mapper define una consulta SQL:
@Select("SELECT COUNT(*) FROM bugs")
int getTotalCount();
Para probar este servicio, se crea una clase de prueba que arranca el contexto de Spring. Se utilizan anotaciones clave: @RunWith(SpringRunner.class) para integrar con JUnit y @SpringBootTest para cargar el ApplicationContext. Un ejemplo básico:
@RunWith(SpringRunner.class)
@SpringBootTest
public class BugServiceImplTest {
@Autowired
private BugServiceImpl bugService;
@Test
public void testCountBugs() {
int total = bugService.countBugs();
System.out.println("Total de bugs: " + total);
}
}
Al ejecutar esta prueba, se realiza una conexión real a la base de datos. Para probar métodos con parámetros, se puede configurar datos de entrada en un método @Before y verificar el comportamiento en diferentes escenarios.
Simulación de dependencias con Mockito
En la práctica, las pruebas unitarias deben aislar el código de dependencias externas. Mockito facilita esto al crear objetos simulados (mocks) que devuelven valores predeterminados. Por ejemplo, para probar el servicio anterior sin acceder a la base de datos:
@RunWith(SpringRunner.class)
@SpringBootTest
public class BugServiceImplMockTest {
@Autowired
@InjectMocks
private BugServiceImpl bugService;
@Mock
private BugMapper bugMapper;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testCountBugsWithMock() {
int mockedCount = 42;
Mockito.when(bugMapper.getTotalCount()).thenReturn(mockedCount);
int result = bugService.countBugs();
Assert.assertEquals(mockedCount, result);
}
}
Aquí, @Mock crea una simulación del mapper, e @InjectMocks inyecta este mock en el servicio bajo prueba. La prueba verifica que el servicio utilice el valor simulado, sin llamar a la base de datos real.
Simulación parcial de dependencias
A veces, es necesario simular solo algunas dependencias mientras se mantienen interacciones reales con otras. Por ejemplo, un servicio que llama a otro servicio y a un mapper. Se puede usar @Mock para el servicio externo y @Autowired para el mapper real:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ReportServiceTest {
@Autowired
@InjectMocks
private ReportServiceImpl reportService;
@Mock
private NotificationService notificationService;
@Autowired
private DataMapper dataMapper; // Interacción real con base de datos
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
// Configurar datos de prueba para el mapper real
}
@Test
public void testGenerateReport() {
Mockito.when(notificationService.sendAlert(Mockito.anyString())).thenReturn(true);
// Prueba que involucra lógica real del mapper y notificaciones simuladas
boolean reportGenerated = reportService.generateReport("test");
Assert.assertTrue(reportGenerated);
}
}
Diferencias entre @Spy y @Mock
La anotación @Mock crea un objeto completamente simulado, ideal cuando la dependencia no está disponible en el contexto. @Spy envuelve un objeto real y permite parcialmente simular métodos específicos. Si la dependencia existe en el proyecto y se necesita comportamiento real por defecto, @Spy es preferible. Por ejemplo:
@Spy
private UserService userService; // Basado en una implementación real
// En la prueba, se pueden simular métodos específicos:
Mockito.doReturn(false).when(userService).isUserActive(Mockito.anyLong());
Sin embargo, si la dependencia no puede ser instanciada (por ejemplo, un servicio externo no desplegado), se debe usar @Mock para evitar errores de inyección.
Configuración reutilizable para pruebas
Para evitar repetir anotaciones comunes en múltiples clases de prueba, se puede crear una clase base:
@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class BaseTest {
@BeforeClass
public static void setupEnvironment() {
System.setProperty("spring.profiles.active", "test");
}
}
Luego, las clases de prueba específicas extienden esta base:
public class BugServiceImplTest extends BaseTest {
// Pruebas específicas aquí
}
Esta aproximación centraliza configuraciones globales y mantiene el código limpio. Para integrar Mockito, la dependencia spring-boot-starter-test ya incluye todo lo necesario, por lo que no se requiere configuración adicional en el archivo pom.xml más allá de lo estándar de Spring Boot.