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
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
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);
}
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
- 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 {
void setUp()
: Este é o cabeçalho do método de configuraçãosetUp()
. 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;
- Aqui, três variáveis
existingId
,nonExistingId
edependentId
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));
bookDTO
: É criado um objetobookDTO
que representa um livro de teste. Provavelmente, esse objeto é criado através de um método estático da classeFactory
, chamadocreateBookDTO()
, que retorna um livro de teste populado com dados fictícios.page
: É criado um objetoPageImpl
que contém obookDTO
anterior.PageImpl
é uma implementação da interfacePage
, frequentemente usada para representar os resultados de paginação. Neste caso, estamos criando uma página com um único livro, que é obookDTO
.
when(service.findAllPaged(any())).thenReturn(page);
- 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 porany()
), ele deve retornar o objetopage
, que contém obookDTO
criado anteriormente.
when(service.findById(existingId)).thenReturn(bookDTO);
when(service.findById(nonExistingId)).thenThrow(ResourceNotFoundException.class);
- Estes trechos configuram o comportamento simulado do método
findById()
do serviço. O primeiro trecho diz que quando o métodofindById()
é chamado com o argumentoexistingId
, ele deve retornar obookDTO
. O segundo trecho diz que quando o método é chamado com o argumentononExistingId
, ele deve lançar uma exceçãoResourceNotFoundException
, indicando que o livro não foi encontrado.
when(service.insert(any())).thenReturn(bookDTO);
- 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 porany()
), ele deve retornar obookDTO
.
when(service.update(eq(existingId), any())).thenReturn(bookDTO);
when(service.update(eq(nonExistingId), any())).thenThrow(ResourceNotFoundException.class);
- Estes trechos configuram o comportamento simulado do método
update()
do serviço. O primeiro trecho diz que quando o métodoupdate()
é chamado comexistingId
como o primeiro argumento e qualquer segundo argumento (representado porany()
), ele deve retornar obookDTO
. O segundo trecho diz que quando o método é chamado comnonExistingId
como o primeiro argumento e qualquer segundo argumento, ele deve lançar uma exceçãoResourceNotFoundException
.
doNothing().when(service).delete(existingId);
doThrow(ResourceNotFoundException.class).when(service).delete(nonExistingId);
doThrow(DatabaseException.class).when(service).delete(dependentId);
- Estes trechos configuram o comportamento simulado do método
delete()
do serviço. O primeiro trecho diz que quando o métododelete()
é chamado com o argumentoexistingId
, nada deve acontecer (método vazio,doNothing()
). O segundo trecho diz que quando o método é chamado com o argumentononExistingId
, ele deve lançar uma exceçãoResourceNotFoundException
. O terceiro trecho diz que quando o método é chamado com o argumentodependentId
, ele deve lançar uma exceçãoDatabaseException
, 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());
}
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:
@Test
: Essa anotação identifica que o método é um teste e pode ser executado pelo framework de testes.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.String jsonBody = objectMapper.writeValueAsString(bookDTO);
: O objetobookDTO
está sendo convertido em uma representação JSON por meio doobjectMapper
. Isso é necessário para enviá-lo no corpo da requisição POST.ResultActions result = mockMvc.perform(...);
: Essa linha executa a requisição POST para o endpoint "/books" com o JSON dobookDTO
como corpo. A resposta da requisição é armazenada emresult
.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.result.andExpect(jsonPath("$.id").exists());
: Verifica se o campo "id" existe na resposta JSON. Isso assegura que o livro criado tem um identificador único.result.andExpect(jsonPath("$.name").exists());
: Verifica se o campo "name" existe na resposta JSON. Isso garante que o livro criado tem um nome.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;
}
@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.@AutoConfigureMockMvc
: Essa anotação é usada para configurar automaticamente o objetoMockMvc
, 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.@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.public class BookResourceIT
: Aqui, estamos declarando uma classe chamadaBookResourceIT
que será responsável por conter os testes de integração relacionados ao recurso de livros.@Autowired
: Essas anotações são usadas para injetar dependências no teste. Neste caso,MockMvc
eObjectMapper
são automaticamente injetados pelo Spring.private MockMvc mockMvc;
: Declaração da variávelmockMvc
, que é o objeto usado para simular e executar solicitações HTTP durante os testes.private ObjectMapper objectMapper;
: Declaração da variávelobjectMapper
, que é uma instância da classeObjectMapper
do Jackson. Ela é utilizada para converter objetos Java em JSON e vice-versa.private Long existingId;
: Declaração da variávelexistingId
, que provavelmente é usada para armazenar o ID de um livro existente no banco de dados para fins de teste.private Long nonExistingId;
: Declaração da variávelnonExistingId
, que é usada para armazenar o ID de um livro que não existe no banco de dados para fins de teste.private Long countTotalBooks;
: Declaração da variávelcountTotalBooks
, que é usada para armazenar a contagem total de livros no banco de dados para fins de teste.@BeforeEach
: Essa anotação é usada para indicar que o métodosetUp()
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.void setUp() throws Exception { ... }
: Aqui, temos o métodosetUp()
, que é responsável por configurar os dados de teste necessários para o contexto do teste. Neste caso, está atribuindo valores aos objetosexistingId
,nonExistingId
ecountTotalBooks
.
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));
}
Vamos analisar o código linha por linha:
@Test
: Esta é uma anotação do JUnit que indica que o método a seguir é um caso de teste.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.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.String jsonBody = objectMapper.writeValueAsString(bookDTO);
: O objeto BookDTO criado é convertido em uma representação JSON em formato de string usando umObjectMapper
. Isso é necessário para enviar os dados para a API durante o teste.String expectedName = bookDTO.getName();
eString 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.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.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.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.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.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)