3 motivos do porquê testes unitários não são suficientes para Microservices com Spring Boot

Jordi Henrique Silva - Dec 14 '22 - - Dev Community

Escrever testes automatizados já é rotina para maioria dos times de tecnologia, porém, o que não te contaram é que testar microsserviços Spring Boot confiando em apenas de testes de unidade pode diminuir a qualidade do seu sistema. E hoje vou mostrar para você 3 motivos que demostram que apenas testes unitários não são suficientes para microsserviços com Spring Boot.

O primeiro motivo é que testes de unidade costumam ser escritos utilizando Mocks, e se você esta instanciando seus Beans manualmente esta deixando de validar uma série de comportamentos e configurações relacionados ao contexto do Spring.

O segundo motivo é que ao mockar as dependências dos seus Beans, você esta renunciando a validar o comportamento entre a integração de um componente e outro. E isto pode fazer com que, falhas de conexão entre seu banco de dados relacional, mapeamentos de entidade da JPA/Hibernate e controle transacional nunca sejam executados. O impacto disto é que se houver algum erro em alguma destas tarefas o seu cliente/usuario é quem vai encontrar o bug em produção.

O terceiro motivo é que você esta deixando de validar os efeitos colaterais do seu componente de código, isto significa, que você não tem nenhuma garantia que inseriu um registro no banco, uma mensagem na fila, ou um arquivo no storage.

Para entender melhor, observe o código abaixo, neste temos uma API REST que recebe informações necessárias para o cadastro de um produto na base de dados.

@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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Um caso de teste de unidade para este Controller, focaria em validar se o contrato esta sendo respeitado, o que significa que instanciaríamos um objeto do tipo ProdutoRequest, e executaríamos a função cadastrar, e por fim iremos validar se a resposta HTTP contém um Status 201 CREATED. Para realizar este teste é necessário que utilizaremos o Mockito para criar um duble do ProdutoRepository, dando comportamento para os métodos: existsBySku e save. Abaixo tem o código correspondente a este caso de teste.

@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() {
        //Cenario
        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());

        //acao
        ResponseEntity<?> response = produtoController.cadastrar(produtoRequest);

        // validacao
        assertEquals(HttpStatus.CREATED, response.getStatusCode());

    }

}
Enter fullscreen mode Exit fullscreen mode

Por mais que pareça que o caso de teste é completo, ele não valida características essências da nossa API. Como se ela atende, ao verbo POST, e a URI de "/produtos". Não valida se as informações recebidas no body da requisição HTTP são desserializadas em um objeto Java. Também não é validado se as informações referentes ao produto são registradas no Banco de Dados.

A verdade é que diversos comportamentos indispensáveis para o funcionamento do software são ignorados no caso de teste. E caso não funcionem como esperado, não serão detectados durante a execução.

Um teste bem escrito para este Controller consideraria a integração com Application Context do Spring Boot, o que garantiria que Beans de todas as camadas seriam instanciados, e se houver erros de configuração e mapeamento o teste já falharia imediatamente. Outra característica essencial é que a lógica de negócio execute de maneira similar a execução do servidor, isto significa que a API deve ser exposta na Web, e que durante o teste uma requisição HTTP deve ser feita, o que garantiria que um banco de dados seria utilizado na execução, ou seja, como efeito colateral da requisição, um registro deve ser criado na base de dados. Abaixo tem o código correspondente a este caso de teste.

@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 {
        //Cenario
        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);

        //Acao
        ResultActions response = mockMvc.perform(request);
        //Validacao
        response.andExpectAll(
                status().isCreated()
        );

        assertEquals(1, repository.findAll().size(),
                "deveria conter apenas um registro de produto"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Como visto anteriormente testes de integração são mais assertivos que testes unitários, pois, testes integrados favorecem que os diversos pontos de integração sejam exercitados. Exercitar a integração com contexto do Spring, favorece que validamos configuração das propriedades e Beans. Também favorece a validação do comportamento de integração as camadas referente a Rede e Banco de Dados.

No contexto de sistemas distribuídos e microsserviços onde temos pequenas bases de código e a maioria das operações são referentes a entrada e saída (I/O) favorecer testes de unidades não será suficientes para garantir o comportamento do seu software.

E se você ainda tem dúvida do que leu até aqui, remova todas as anotações de um Controller e rode seus testes de unidade, e se nenhum teste quebrar me conta aqui nos comentários.

. . . . . . . . . . . .