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)