El primer motivo es que las pruebas unitarias suelen escribirse utilizando Mocks, y si estás instanciando tus Beans manualmente, dejas de validar una serie de comportamientos y configuraciones relacionados con el contexto de Spring.
El segundo motivo es que al mockear las dependencias de tus Beans, renuncias a validar el comportamiento entre la integración de un componente y otro. Esto puede hacer que las fallas de conexión entre tu base de datos relacional, los mapeos de entidades de JPA/Hibernate y el control transaccional nunca se ejecuten. El impacto de esto es que, si hay algún error en alguna de estas tareas, será tu cliente/usuario quien encontrará el bug en producción.
El tercer motivo es que dejas de validar los efectos colaterales de tu componente de código, lo que significa que no tienes ninguna garantía de que se haya insertado un registro en la base de datos, un mensaje en la cola o un archivo en el almacenamiento.
Para entender mejor, observa el código a continuación, donde tenemos una API REST que recibe información necesaria para el registro de un producto en la base de datos.
@RestController
public class CadastraProdutoController {
private final ProdutoRepository repository;
private final Logger LOGGER = LoggerFactory.getLogger(CadastraProdutoController.class);
public CadastraProdutoController(ProdutoRepository repository) {
this.repository = repository;
}
@PostMapping("/produtos")
@Transactional
public ResponseEntity<?> cadastrar(@RequestBody @Valid ProdutoRequest request) {
if (repository.existsBySku(request.getSku())) {
LOGGER.error("Já existe um produto cadastrado para este sku {}", request);
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Produto ja cadastrado");
}
Produto novoProduto = request.toModel();
repository.save(novoProduto);
LOGGER.info("Produto Cadastrado {}", novoProduto);
return ResponseEntity.status(CREATED).build();
}
}
Un caso de prueba unitaria para este Controller se enfocaría en validar si el contrato se está respetando, lo que significa que instanciaríamos un objeto del tipo ProductoRequest, ejecutaríamos la función cadastrar
, y finalmente validaríamos si la respuesta HTTP contiene un Status 201 CREATED. Para realizar esta prueba es necesario utilizar Mockito para crear un doble del ProductoRepository, dando comportamiento a los métodos: existsBySku
y save
. A continuación, el código correspondiente a este caso de prueba.
@ExtendWith(value = MockitoExtension.class)
class CadastraProdutoControllerUnitTest {
@Mock
private ProdutoRepository repository;
private CadastraProdutoController produtoController;
@BeforeEach
void setUp() {
this.produtoController = new CadastraProdutoController(repository);
}
@Test
@DisplayName("deve cadastrar um Produto")
void t1() {
// Escenario
ProdutoRequest produtoRequest = new ProdutoRequest(
"PlayStation 5",
"Console e 2 controles",
BigDecimal.TEN,
"123567"
);
when(repository.existsBySku(produtoRequest.getSku())).thenReturn(false);
when(repository.save(any(Produto.class))).thenReturn(produtoRequest.toModel());
// Acción
ResponseEntity<?> response = produtoController.cadastrar(produtoRequest);
// Validación
assertEquals(HttpStatus.CREATED, response.getStatusCode());
}
}
Aunque parezca que el caso de prueba está completo, no valida características esenciales de nuestra API, como si respeta el verbo POST y la URI "/produtos". No valida si la información recibida en el cuerpo de la solicitud HTTP se deserializa en un objeto Java. Tampoco se valida si la información referente al producto se registra en la base de datos.
La verdad es que diversos comportamientos indispensables para el funcionamiento del software son ignorados en el caso de prueba. Y si no funcionan como se espera, no se detectarán durante la ejecución.
Una prueba bien escrita para este Controller consideraría la integración con el Application Context de Spring Boot, lo que garantizaría que los Beans de todas las capas se instancien, y si hay errores de configuración y mapeo, la prueba fallará inmediatamente. Otra característica esencial es que la lógica de negocio se ejecute de manera similar a la ejecución del servidor, lo que significa que la API debe estar expuesta en la Web, y que durante la prueba se haga una solicitud HTTP, lo que garantizaría que se use una base de datos en la ejecución, es decir, como efecto colateral de la solicitud, se debe crear un registro en la base de datos. A continuación, el código correspondiente a este caso de prueba.
@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureMockMvc(printOnlyOnFailure = false)
class CadastraProdutoControllerIntegrationTest {
@Autowired
private ObjectMapper mapper;
@Autowired
private ProdutoRepository repository;
@Autowired
private MockMvc mockMvc;
@BeforeEach
void setUp() {
repository.deleteAll();
}
@Test
@DisplayName("deve cadastrar um Produto")
void t1() throws Exception {
// Escenario
ProdutoRequest produtoRequest = new ProdutoRequest(
"PlayStation 5",
"Console e 2 controles",
BigDecimal.TEN,
"123456"
);
String payload = toJson(produtoRequest);
MockHttpServletRequestBuilder request = post("/produtos")
.header(HttpHeaders.ACCEPT_LANGUAGE, "en")
.contentType(APPLICATION_JSON)
.content(payload);
// Acción
ResultActions response = mockMvc.perform(request);
// Validación
response.andExpectAll(
status().isCreated()
);
assertEquals(1, repository.findAll().size(),
"deveria conter apenas um registro de produto"
);
}
}
Como se vio anteriormente, las pruebas de integración son más precisas que las pruebas unitarias, ya que las pruebas integradas favorecen que se ejerciten los diversos puntos de integración. Ejercitar la integración con el contexto de Spring favorece la validación de la configuración de las propiedades y Beans. También favorece la validación del comportamiento de integración en las capas referentes a la red y la base de datos.
En el contexto de sistemas distribuidos y microservicios, donde tenemos pequeñas bases de código y la mayoría de las operaciones son referentes a entrada y salida (I/O), favorecer las pruebas unitarias no será suficiente para garantizar el comportamiento de tu software.
Jorge García
Fullstack developer