DEV Community

Benjamim Alves
Benjamim Alves

Posted on

Testes de Integração e Unidade em uma Aplicação CRUD com Spring Boot

Olá pessoal! 👋

Nesta postagem irei demonstrar e comentar de forma bem detalhada os testes de integração e de unidade em uma aplicação CRUD (Create, Read, Update, Delete) desenvolvida com o framework Spring Boot.

Por fins de brevidade não comentario todos os métodos de cada classe, mas somente o Setup do ambiente de testes e uma função de testes

Recursos
Testes feitos sobre o codigo base:
CRUD BASE

CRUD com TESTS

Ambos os projetos estão bem documentados e comentados

Testes de Integração e Testes Unitários - Uma Visão Geral

Testes de Unidade (Unitários)

Os testes de unidade são a base da estratégia de testes em desenvolvimento de software. Nesse tipo de teste, cada unidade individual de código é isoladamente testada para garantir que ela funcione corretamente. Uma unidade pode ser uma função, método ou classe pequena e específica.

Características dos Testes Unitários:

  • São rápidos e focados em pequenas partes do código.
  • Não dependem de recursos externos, como bancos de dados ou serviços.
  • Podem ser repetidos com facilidade e rapidez.

Testes de Integração

Os testes de integração verificam se as diferentes partes de um sistema funcionam corretamente juntas, garantindo a correta comunicação e interação entre essas partes. Em geral, os testes de integração se concentram em cenários onde múltiplas unidades de código se conectam.

Características dos Testes de Integração:

  • Podem abranger várias unidades e componentes interagindo.
  • Testam a integração com recursos externos, como bancos de dados, APIs e serviços.
  • São mais lentos e complexos do que os testes de unidade.

Aplicação CRUD-BASE

Não será necessário o conhecimento interno de como a nossa aplicação CRUD funciona internamente, você pode encontrar boa parte das informações do funcionamento na própria documentação que eu disponibilizei no github

diretorio de testes

+---java
    +---com
        +---bookCatalog
            +---bookcatalog
                |   BookcatalogApplicationTests.java
                |   
                +---repositories
                |       BookRepositoryTests.java
                |       
                +---resources
                |       BookResourceIT.java
                |       BookResourceTests.java
                |       
                +---services
                |       BookServiceIT.java
                |       BookServiceTests.java
                |       
                +---tests
                        Factory.java
Enter fullscreen mode Exit fullscreen mode

Como se pode observar, fiz testes tanto para o controlador (Resource), como para os serviços e para o repositorio.

Iniciaremos pelo Controlador devido a ele ser o primeiro a receber a requisição após ser roteada pelo Spring DispatcherServlet.

Test Unitário do controlador - BookServiceTests.java

Configuração do ambiente

@BeforeEach
    void setUp() throws Exception {

        // Inicializa dados de teste
        existingId = 1L;
        nonExistingId = 2L;
        dependentId = 3L;

        bookDTO = Factory.createBookDTO();
        page = new PageImpl<>(List.of(bookDTO));

        // Configura o comportamento simulado do serviço para cada caso de teste
        when(service.findAllPaged(any())).thenReturn(page);
        when(service.findById(existingId)).thenReturn(bookDTO);
        when(service.findById(nonExistingId)).thenThrow(ResourceNotFoundException.class);
        when(service.insert(any())).thenReturn(bookDTO);
        when(service.update(eq(existingId), any())).thenReturn(bookDTO);
        when(service.update(eq(nonExistingId), any())).thenThrow(ResourceNotFoundException.class);
        doNothing().when(service).delete(existingId);
        doThrow(ResourceNotFoundException.class).when(service).delete(nonExistingId);
        doThrow(DatabaseException.class).when(service).delete(dependentId);
    }

Enter fullscreen mode Exit fullscreen mode

Este trecho de código é um método chamado setUp(), que faz parte de um ambiente de teste (test suite) em Java. Ele é usado para configurar o estado inicial do ambiente de teste antes de executar cada caso de teste relacionado ao serviço de livros (ou "book service").

Vamos analisar o código linha por linha e explicar o que cada parte faz:

@BeforeEach
Enter fullscreen mode Exit fullscreen mode
  • A anotação @BeforeEach é uma anotação do framework de testes JUnit, uma das bibliotecas de testes mais populares em Java. Ela é usada para marcar um método que será executado antes de cada caso de teste em uma classe de teste específica.
  • Quando o JUnit executa uma classe de teste, ele procura por métodos anotados com @BeforeEach e os executa antes de cada método de teste marcado com @test na mesma classe. Isso permite que os testes sejam executados de forma isolada, sem interferências de um caso de teste no outro, já que o estado do ambiente é configurado novamente antes de cada teste.
void setUp() throws Exception {
Enter fullscreen mode Exit fullscreen mode
  • void setUp(): Este é o cabeçalho do método de configuração setUp(). Ele não retorna nenhum valor (void) e não recebe nenhum argumento.

  • throws Exception: Indica que o método pode lançar uma exceção (neste caso, qualquer exceção não verificada). Isso pode ser necessário para lidar com erros em algumas das operações realizadas durante a configuração do ambiente de teste.

existingId = 1L;
nonExistingId = 2L;
dependentId = 3L;
Enter fullscreen mode Exit fullscreen mode
  • Aqui, três variáveis existingId, nonExistingId e dependentId são inicializadas com valores numéricos (long) - 1, 2 e 3, respectivamente. Esses valores são usados em diferentes casos de teste para representar IDs de livros existentes, IDs de livros não existentes e IDs de livros dependentes, que serão usados para simular diferentes cenários.
bookDTO = Factory.createBookDTO();
page = new PageImpl<>(List.of(bookDTO));
Enter fullscreen mode Exit fullscreen mode
  • bookDTO: É criado um objeto bookDTO que representa um livro de teste. Provavelmente, esse objeto é criado através de um método estático da classe Factory, chamado createBookDTO(), que retorna um livro de teste populado com dados fictícios.

  • page: É criado um objeto PageImpl que contém o bookDTO anterior. PageImpl é uma implementação da interface Page, frequentemente usada para representar os resultados de paginação. Neste caso, estamos criando uma página com um único livro, que é o bookDTO.

when(service.findAllPaged(any())).thenReturn(page);
Enter fullscreen mode Exit fullscreen mode
  • Este trecho configura o comportamento simulado do método findAllPaged() do serviço. Ele define que, quando esse método é chamado com qualquer argumento (representado por any()), ele deve retornar o objeto page, que contém o bookDTO criado anteriormente.
when(service.findById(existingId)).thenReturn(bookDTO);
when(service.findById(nonExistingId)).thenThrow(ResourceNotFoundException.class);
Enter fullscreen mode Exit fullscreen mode
  • Estes trechos configuram o comportamento simulado do método findById() do serviço. O primeiro trecho diz que quando o método findById() é chamado com o argumento existingId, ele deve retornar o bookDTO. O segundo trecho diz que quando o método é chamado com o argumento nonExistingId, ele deve lançar uma exceção ResourceNotFoundException, indicando que o livro não foi encontrado.
when(service.insert(any())).thenReturn(bookDTO);
Enter fullscreen mode Exit fullscreen mode
  • Este trecho configura o comportamento simulado do método insert() do serviço. Ele define que, quando o método é chamado com qualquer argumento (representado por any()), ele deve retornar o bookDTO.
when(service.update(eq(existingId), any())).thenReturn(bookDTO);
when(service.update(eq(nonExistingId), any())).thenThrow(ResourceNotFoundException.class);
Enter fullscreen mode Exit fullscreen mode
  • Estes trechos configuram o comportamento simulado do método update() do serviço. O primeiro trecho diz que quando o método update() é chamado com existingId como o primeiro argumento e qualquer segundo argumento (representado por any()), ele deve retornar o bookDTO. O segundo trecho diz que quando o método é chamado com nonExistingId como o primeiro argumento e qualquer segundo argumento, ele deve lançar uma exceção ResourceNotFoundException.
doNothing().when(service).delete(existingId);
doThrow(ResourceNotFoundException.class).when(service).delete(nonExistingId);
doThrow(DatabaseException.class).when(service).delete(dependentId);
Enter fullscreen mode Exit fullscreen mode
  • Estes trechos configuram o comportamento simulado do método delete() do serviço. O primeiro trecho diz que quando o método delete() é chamado com o argumento existingId, nada deve acontecer (método vazio, doNothing()). O segundo trecho diz que quando o método é chamado com o argumento nonExistingId, ele deve lançar uma exceção ResourceNotFoundException. O terceiro trecho diz que quando o método é chamado com o argumento dependentId, ele deve lançar uma exceção DatabaseException, indicando que ocorreu um erro ao excluir um livro dependente.

Em resumo, esse trecho de código configura um ambiente de teste para o serviço de livros, simulando o comportamento do serviço para diferentes casos de teste. Isso é feito usando o framework de mock Mockito, que permite definir comportamentos simulados para os métodos do serviço. Essa configuração é útil para testar o serviço de livros isoladamente, sem depender de um banco de dados real ou outras dependências externas.

Função de teste

@Test
    public void insertShouldReturnBookDTOCreated() throws Exception {

        String jsonBody = objectMapper.writeValueAsString(bookDTO);

        ResultActions result =
                mockMvc.perform(post("/books")
                        .content(jsonBody)
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON));

        result.andExpect(status().isCreated());
        result.andExpect(jsonPath("$.id").exists());
        result.andExpect(jsonPath("$.name").exists());
        result.andExpect(jsonPath("$.description").exists());
    }
Enter fullscreen mode Exit fullscreen mode

Utilizando a biblioteca JUnit para escrever testes automatizados. O objetivo do teste é verificar se a criação de um livro (representado pelo objeto bookDTO) na API REST está funcionando corretamente, ou seja, se, ao enviar uma requisição POST para o endpoint "/books" com o JSON representando um livro, o servidor responde com o código HTTP 201 (Created) e se o JSON de resposta possui os campos "id", "name" e "description".

Vamos analisar o código linha por linha:

  1. @Test: Essa anotação identifica que o método é um teste e pode ser executado pelo framework de testes.

  2. public void insertShouldReturnBookDTOCreated() throws Exception: Declaração do método de teste. Ele testará a criação de um livro e o retorno dos campos esperados.

  3. String jsonBody = objectMapper.writeValueAsString(bookDTO);: O objeto bookDTO está sendo convertido em uma representação JSON por meio do objectMapper. Isso é necessário para enviá-lo no corpo da requisição POST.

  4. ResultActions result = mockMvc.perform(...);: Essa linha executa a requisição POST para o endpoint "/books" com o JSON do bookDTO como corpo. A resposta da requisição é armazenada em result.

  5. result.andExpect(status().isCreated());: Verifica se o código de status HTTP da resposta é 201 (Created). Isso garante que o livro foi criado com sucesso.

  6. result.andExpect(jsonPath("$.id").exists());: Verifica se o campo "id" existe na resposta JSON. Isso assegura que o livro criado tem um identificador único.

  7. result.andExpect(jsonPath("$.name").exists());: Verifica se o campo "name" existe na resposta JSON. Isso garante que o livro criado tem um nome.

  8. result.andExpect(jsonPath("$.description").exists());: Verifica se o campo "description" existe na resposta JSON. Isso garante que o livro criado tem uma descrição.

Em resumo, esse trecho de código realiza um teste automatizado para verificar se a criação de um livro através de uma requisição POST na API REST está funcionando conforme o esperado, retornando um código HTTP 201 (Created) e com os campos "id", "name" e "description" presentes no JSON de resposta.

Test de Integração do controlador - BookResourceIT.java

Configuração do ambiente

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class BookResourceIT {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    private Long existingId;
    private Long nonExistingId;
    private Long countTotalBooks;

    @BeforeEach
    void setUp() throws Exception {
        existingId = 1L;
        nonExistingId = 1000L;
        countTotalBooks = 25L;
    }
Enter fullscreen mode Exit fullscreen mode
  1. @SpringBootTest: Essa anotação é usada para carregar e configurar o contexto de aplicativo Spring para o teste de integração. Ele permite que o teste acesse os beans gerenciados pelo Spring e imita o ambiente de execução da aplicação.

  2. @AutoConfigureMockMvc: Essa anotação é usada para configurar automaticamente o objeto MockMvc, que é uma classe fornecida pelo Spring para simular as solicitações HTTP e testar os controladores sem a necessidade de fazer chamadas reais pela rede.

  3. @Transactional: Essa anotação é usada para garantir que cada método de teste seja executado dentro de uma transação e seja revertido após a conclusão do teste. Isso ajuda a manter o banco de dados em um estado consistente entre os testes.

  4. public class BookResourceIT: Aqui, estamos declarando uma classe chamada BookResourceIT que será responsável por conter os testes de integração relacionados ao recurso de livros.

  5. @Autowired: Essas anotações são usadas para injetar dependências no teste. Neste caso, MockMvc e ObjectMapper são automaticamente injetados pelo Spring.

  6. private MockMvc mockMvc;: Declaração da variável mockMvc, que é o objeto usado para simular e executar solicitações HTTP durante os testes.

  7. private ObjectMapper objectMapper;: Declaração da variável objectMapper, que é uma instância da classe ObjectMapper do Jackson. Ela é utilizada para converter objetos Java em JSON e vice-versa.

  8. private Long existingId;: Declaração da variável existingId, que provavelmente é usada para armazenar o ID de um livro existente no banco de dados para fins de teste.

  9. private Long nonExistingId;: Declaração da variável nonExistingId, que é usada para armazenar o ID de um livro que não existe no banco de dados para fins de teste.

  10. private Long countTotalBooks;: Declaração da variável countTotalBooks, que é usada para armazenar a contagem total de livros no banco de dados para fins de teste.

  11. @BeforeEach: Essa anotação é usada para indicar que o método setUp() deve ser executado antes de cada método de teste. Isso é útil para configurar os dados de teste necessários antes da execução dos testes.

  12. void setUp() throws Exception { ... }: Aqui, temos o método setUp(), que é responsável por configurar os dados de teste necessários para o contexto do teste. Neste caso, está atribuindo valores aos objetos existingId, nonExistingId e countTotalBooks.

Função de teste

@Test
    public void updateShouldReturnBookDTOWhenIdExists() throws Exception {

        BookDTO bookDTO = Factory.createBookDTO();
        String jsonBody = objectMapper.writeValueAsString(bookDTO);

        String expectedName = bookDTO.getName();
        String expectedDescription = bookDTO.getDescription();

        ResultActions result = 
                mockMvc.perform(put("/books/{id}", existingId)
                    .content(jsonBody)
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON));

        result.andExpect(status().isOk());
        result.andExpect(jsonPath("$.id").value(existingId));
        result.andExpect(jsonPath("$.name").value(expectedName));
        result.andExpect(jsonPath("$.description").value(expectedDescription));
    }
Enter fullscreen mode Exit fullscreen mode

Vamos analisar o código linha por linha:

  1. @Test: Esta é uma anotação do JUnit que indica que o método a seguir é um caso de teste.

  2. public void updateShouldReturnBookDTOWhenIdExists() throws Exception: Esta é a assinatura do método do caso de teste. Ele indica que o método verifica se a atualização de um livro retorna um objeto BookDTO quando o ID do livro já existe.

  3. BookDTO bookDTO = Factory.createBookDTO();: Aqui, um objeto BookDTO é criado usando um Factory (fábrica) para obter dados fictícios de um livro. Isso é feito para simular um objeto de livro que será usado como entrada para a atualização.

  4. String jsonBody = objectMapper.writeValueAsString(bookDTO);: O objeto BookDTO criado é convertido em uma representação JSON em formato de string usando um ObjectMapper. Isso é necessário para enviar os dados para a API durante o teste.

  5. String expectedName = bookDTO.getName(); e String expectedDescription = bookDTO.getDescription();: São extraídos o nome e a descrição esperados do objeto BookDTO. Esses valores serão usados posteriormente nas asserções para verificar se a resposta da API contém os valores corretos.

  6. ResultActions result = mockMvc.perform(put("/books/{id}", existingId) ... );: Nesta linha, é feita uma requisição PUT para a URL "/books/{id}", onde "{id}" é um marcador de posição que será substituído pelo valor de "existingId". Isso simula a chamada para atualizar um livro específico na API. O corpo da requisição conterá o JSON do objeto BookDTO criado anteriormente.

  7. result.andExpect(status().isOk());: Esta asserção verifica se a resposta da API possui o status HTTP 200 (OK). Isso garante que a atualização foi bem-sucedida e que a resposta é válida.

  8. result.andExpect(jsonPath("$.id").value(existingId));: Essa asserção verifica se o valor do campo "id" na resposta JSON é igual ao valor de "existingId". Isso é importante para garantir que o livro atualizado tenha o mesmo ID que o livro que foi modificado.

  9. result.andExpect(jsonPath("$.name").value(expectedName));: Esta asserção verifica se o valor do campo "name" na resposta JSON é igual ao valor esperado, que foi extraído do objeto BookDTO criado anteriormente.

  10. result.andExpect(jsonPath("$.description").value(expectedDescription));: Essa asserção verifica se o valor do campo "description" na resposta JSON é igual ao valor esperado, que também foi extraído do objeto BookDTO criado anteriormente.

Essas asserções garantem que a API está retornando corretamente os dados do livro atualizado em um formato JSON, com os campos "id", "name" e "description" contendo os valores esperados. Se todas as asserções passarem, o teste será considerado bem-sucedido.

Top comments (0)