DEV Community

Jordi Henrique Silva
Jordi Henrique Silva

Posted on • Updated 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 (12)

Collapse
 
lobocode profile image
Vitor Lobo

E aí @jordi Henrique Silva, tudo bom? @loboriseup aqui (twitter). No primeiro item, no caso, primeiro motivo, há um pequeno equívoco no argumento. Explico: dá uma olhada neste artigo escrito pelo Baeldung (baeldung.com/injecting-mocks-in-sp...). Nele, é demonstrado como usar a injeção de dependência para inserir mocks Mockito em beans do Spring para testes unitários. O objetivo dessa abordagem é fornecer isolamento adequado aos testes, permitindo que se foque na funcionalidade de uma unidade específica sem envolver toda a hierarquia de classes em cada teste. Exemplo:

@ActiveProfiles("test")
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MocksApplication.class)
public class UserServiceUnitTest {
    @Autowired
    private UserService userService;

    @Autowired
    private NameService nameService;

    @Test
    public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
        Mockito.when(nameService.getUserName("SomeId")).thenReturn("Mock user name");
        String testName = userService.getUserName("SomeId");
        Assert.assertEquals("Mock user name", testName);
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui é um exemplo de como é possível utilizar mocks Mockito em beans do Spring para testes unitários, validando comportamentos e configurações relacionados ao contexto do Spring. Através da injeção de mocks, é possível isolar a unidade de código em teste e verificar seu comportamento de forma específica, sem envolver as dependências reais do Spring. Portanto, o argumento de que os testes de unidade com mocks não validam comportamentos e configurações relacionados ao contexto do Spring não é válido, pois é possível alcançar essa validação por meio dessa abordagem de teste unitário com mocks Mockito e injeção de dependência no Spring.

Collapse
 
jordihofc profile image
Jordi Henrique Silva

Obrigado por compartilhar @lobocode, acho que o primeiro ponto que temos que discutir aqui é o que são testes unitários e o que são testes de integração.

Testes de unidade são aqueles que testamos nossas classes de forma isolada, o que significa que é minha classe se comunica apenas com código que eu ou meu time escreveu. Já os testes de integração que me refiro no artigo são aquelas que se integram com dependências externas, como o próprio contexto do Spring.

Acho que também é importante ressaltar que o código demostrado no artigo do baeldung favorece que você injete Mocks no contexto do Spring, o que é feito hoje através de @MockBean. Um detalhe a se notar é que quando você sobe o Contexto do Spring em um ambiente de teste você NÃO está testando de forma unitária, mas sim de forma integrada. E fazendo com que os Beans sejam criados e integrados ao Container de IoC/DI.

Collapse
 
lobocode profile image
Vitor Lobo • Edited

Jordi, olha só, sobre o segundo motivo acima, novamente no Baeldung vc encontra este artigo aqui: baeldung.com/spring-test-pyramid-p.... No link é abordado diferentes tipos de testes e a importância de cada um deles. De acordo com esse artigo, existem três tipos principais de testes: testes unitários, testes de integração e testes de interface do usuário (UI). Ou seja, teste unitário e teste de integração tem abordagens e objetivos diferentes Jordi.

Observe que a abordagem do teste de pirâmide, proposta por Mike Cohn, sugere que escrevamos testes com diferentes níveis de granularidade e quantidade, com maior ênfase nos testes unitários e menor quantidade de testes conforme ampliamos o escopo dos testes. A ideia é que a quantidade de testes siga a forma de uma pirâmide, sendo maior nos níveis mais granulares e diminuindo conforme avançamos para testes mais amplos.

Com base no artigo do Baeldung, podemos concluir que testes unitários, testes de integração e testes de UI têm suas próprias finalidades e devem ser combinados de forma adequada para garantir uma cobertura abrangente dos testes em um contexto de microsserviços com Spring Boot. Se quiser trocar uma ideia, me segue lá bolha.us/@lobocode.

Collapse
 
jordihofc profile image
Jordi Henrique Silva • Edited

A pirâmide de testes é uma abordagem de fato muito utilizada e auxilia desenvolvedores e times de tecnologia a traçar estratégias para escrita de uma suíte de testes para o sistema.

O fato que deve ser notado aqui é que no contexto de microsserviços onde temos pequenas bases de código, operações que se concentram em I/O, Magias feitas por baixo dos panos através de IoC/DI os testes de unidade NÃO serão suficientes. E para estes casos a sugestão é FAVORECER os teste de integração. Então antes quando você tinha maior quantidade de testes de unidade, agora você pode redistribuir a proporção, talvez formando um losangolo.

Se quiser saber mais recomendo que leia:
martinfowler.com/articles/2021-tes...

E se quiser continuar discutindo sobre me segue lá no twitter:

@JordiHSilva

Collapse
 
lobocode profile image
Vitor Lobo

Que mancada a minha de confundir Fowler com Uncle Bob.

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
 
eguadorodrigo profile image
Rodrigo

Primeiro, parabéns pelo artigo!
Provocação: em se tratando do dia-a-dia, como seriam postos a responsabilidade dentro de um time?
Olhando pra tal pirâmide de testes, o de unidade os de integração e os ponta a ponta, cada um além de ter seus próprios objetivos, ao meu ver, também teriam responsáveis diferentes. É importante entender, aplicar, mas acho válido a separação de até onde vai a responsabilidade de implementar cada teste, vide que em uma equipe é desejável ter um time multidisciplinar.
Mas concordo com a premissa de que testes devem ter os três aspectos para alavancar a qualidade da aplicação: cobertura/integração/e2e.
E ainda assim, as aplicações possuem outras métricas, contextos de infra e é responsabilidade de todo time, buscar a excelência.
Enfim, ótimo tópico e abordagem, exemplos e didática!

Collapse
 
jordihofc profile image
Jordi Henrique Silva

@rodrigo obrigado por compartilhar suas provocações!

em se tratando do dia-a-dia, como seriam postos a responsabilidade dentro de um time?

Antes de descrever como vejo a divisão de responsabilidades no dia a dia, acho importante alinhar o que entendo como teste de integração.

Para mim, teste de integração é quando valido a integração do código que escrevi com qualquer outro código, inclusive do framework que estou utilizando. Olhando para isso, acho que tanto os testes de unidade quanto os de integração devem ser escritos e mantidos pelos desenvolvedores.

Olhando para times multi-disciplinares, vejo que os testes de aceitação e ponta a ponta podem ser transferidos para o QA ou QE, que possuem mais técnicas para escrita de testes, inclusive os não funcionais.

Collapse
 
andersonsantana profile image
Anderson Santana

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