Testes de Integração e Unidade em uma Aplicação CRUD com Spring Boot
Published: 2023-08-02 19:04:49

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](https://github.com/Benjamim-EP/Springboot-CRUD)

[CRUD com TESTS](https://github.com/Benjamim-EP/SpringBoot-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

```markdown
+---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

```java
@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:

```java
@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.

```java
void setUp() throws Exception {
```

- `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.

```java
existingId = 1L;
nonExistingId = 2L;
dependentId = 3L;
```

- 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.

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

- `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`.

```java
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 por `any()`), ele deve retornar o objeto `page`, que contém o `bookDTO` criado anteriormente.

```java
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é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.

```java
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 por `any()`), ele deve retornar o `bookDTO`.

```java
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é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`.

```java
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é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

```java
@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:

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

```java
@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;
	}
```


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

```java
@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:

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.