Volver a la página principal
martes 30 julio 2024
14

3 motivos por los que las pruebas unitarias no son suficientes para microservicios con Spring Boot

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"
        );
    }
}

Conclusión

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.

Compartir:
Creado por:
Author photo

Jorge García

Fullstack developer