DEV Community

Jordi Henrique Silva
Jordi Henrique Silva

Posted on

 

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

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.

Top comments (5)

Collapse
 
dearrudam profile image
Maximillian Arruda

Muito massa o artigo!!! Quanto mais utilizamos bibliotecas e frameworks como Bean Validations ou JPA por exemplo, testes de integração são necessários para ter certeza quanto ao comportamento e execução da aplicação...ainda bem que o Spring, Quarkus e outros ferramentas estão cada vez mais nos ajudando com essas tarefas!!! Sim, sei que há casos que não há necessidade de subir um ambiente integrado para testar alguma regra de negócio, mas acho que testes de integração expõe uma perpectiva muito mais próxima de uma situação em produção!!! Parabéns pelo artigo!!!

Collapse
 
jordihofc profile image
Jordi Henrique Silva

Muito obrigado por expor seu ponto de vista Max!
Os testes integrados oferecem feedbacks mais fieis quanto o comportamento do software em cenários de produção.
Ainda mais que hoje, temos pequenas bases de código que na maioria das vezes são focados em fazer I/O.

Collapse
 
viniciusxyz profile image
Vinicius Vieira dos Santos

Concordo com sua visão sobre não ser suficiente, mas eu penso que testes unitários são mais simples de fazer e executar justamente pq envolvem poucos componentes ( afinal é UNitário ) e quando temos um fluxo de testes automatizados conseguimos pegar uma boa quantidade de bugs com eles antes de chegar a etapa do teste integrado por isso creio que devem ser os primeiros a serem escritos, uma boa cobertura de testes unitários provê muiito mais confiança no novo código. O texto descreve bem os problemas que temos quando usamos apenas testes unitários, mas sinto que uma pessoa que tem menos experiência pode acabar lendo ele como "abandonem os testes unitários o negócio agora é teste integrado" o que considero problemático, então valeria um adendo da importância deles, nos últimos anos tenho seguido a política de usar ambas as formas de testar a qualidade do produto e tem dado muito certo, inclusive sinto que onde consegui captar mais erros de forma mais barata foi nos testes unitários.

Collapse
 
jordihofc profile image
Jordi Henrique Silva

Obrigado por expor sua visão Vinicius! E sim, os testes unitários têm seu valor, uma boa suíte deve ser composta por ambas as categorias.

A motivação do artigo é demostrar que no contexto de microsserviços onde a maior proporção do código é composta por validação e operações de I/O, os testes unitários não conseguem identificar se características essenciais do software funcionam como deveria.
Por exemplo, caso uma anotação seja usada equivocadamente, ou se for removida
o teste não irá falhar, ou seja, uma falsa impressão de segurança sera dada.

Dado a isso, o artigo sugere favorecer mais testes integrados do que unitários para este contexto.

Collapse
 
andersonsantana profile image
Anderson Santana

Mano, já passei por projetos onde só era feito testes unitários da use case, achava aquilo bizarro.

An Animated Guide to Node.js Event Loop

Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

What happens under the hood when Node.js works on tasks such as database queries? We will explore it by following this piece of code step by step.