Article image
Leandro Alves
Leandro Alves24/09/2024 18:04
Compartilhe

Testes de unidade em Java

  • #Java

À medida que vou estudando e aprendendo coisas novas, vou criando novos artigos para que pessoas que também estão nessa jornada possam ter uma base sobre o que aprendi. A ideia é compartilhar conhecimento e facilitar o aprendizado, contribuindo para a comunidade e ajudando outros a superarem desafios semelhantes.

O que são testes em Java?

Testes em Java são códigos que verificam se diferentes partes da aplicação funcionam como esperado. Existem vários tipos de testes, cada um com um objetivo específico:

  • Testes de Unidade: Verificam partes pequenas e isoladas do código, como um método específico (por exemplo, testar se uma função de soma está correta).
  • Testes de Integração: Avaliam se diferentes componentes do sistema, como um serviço e um banco de dados, interagem corretamente entre si.
  • Testes Funcionais: Simulam o ponto de vista do usuário, verificando se a aplicação atende aos requisitos funcionais.
  • Testes de Sistema: Testam o sistema inteiro em diferentes ambientes para garantir que tudo funcione corretamente.

Testes são essenciais porque garantem a qualidade e confiabilidade do código, ajudando a encontrar problemas logo no início, documentando o comportamento esperado e mantendo o sistema seguro mesmo com atualizações.

Em Java, geralmente é usado ferramentas como JUnit ou TestNG para criar e executar testes. Esses frameworks permitem automatizar os testes, ou seja, você pode rodar todos os testes rapidamente sempre que fizer uma alteração no código. Nos meus códigos de exemplo usarei o JUnit. Usarei como exemplo, um programa de gerenciamento de treinos de academia que estou desenvolendo.

JUnit 

JUnit é um framework usado para realizar testes em aplicações Java, permitindo que você escreva testes de unidade para funções ou classes. Antes de escrever os testes, é importante configurar o ambiente de desenvolvimento. No site do JUnit (https://junit.org/junit4/dependency-info.html), você pode encontrar as dependências necessárias para o projeto. Como estou usando o Apache Maven, adicionei e atualizei essas dependências no arquivo pom.xml.

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.13.2</version>
</dependency>

Mockito

O Mockito é uma biblioteca usada para criar mocks (objetos simulados) em testes unitários, permitindo testar uma classe isoladamente ao simular o comportamento de suas dependências externas, como serviços ou bancos de dados. Com o Mockito, você define como essas dependências devem agir durante o teste, sem precisar usar implementações ou dados reais. Isso permite simular classes, entidades, repositórios e serviços, facilitando os testes sem interferir no ambiente ou nos dados armazenados.

Começando os testes com JUnit e Mockito

Após configurar e atualizar o ambiente, é hora de começar a escrever os testes. Os testes serão escritos na pasta SRC>TEST, onde organizaremos nossos testes.

image

No meu serviço de exercícios, tenho uma função que busca os exercícios cadastrados pelo ID. Primeiro, vou testar se a função realmente retorna um exercício que está cadastrado. Em seguida, precisarei verificar se a função lança duas exceções personalizadas corretamente quando ocorre algum erro. 

public ExerciseDTO findExerciseById(Long id) {
  Exercise exerciseEntity = exerciseRepository.findById(id)
          .orElseThrow(() -> new NotFoundException("Exercício não encontrado com o id: " + id));
  try {
      ExerciseDTO exerciseDTO = new ExerciseDTO();

      exerciseDTO.setId(exerciseEntity.getId());
      exerciseDTO.setName(exerciseEntity.getName());
      exerciseDTO.setDescription(exerciseEntity.getDescription());
      exerciseDTO.setEquipment(exerciseEntity.getEquipment());
      exerciseDTO.setMuscleGroup(exerciseEntity.getMuscleGroup());

      return exerciseDTO;
  } catch (Exception ex) {
      throw new BusinessException("Erro ao localizar os exercicios!");
  }
}

Boas praticas para nomes de testes

A escolha adequada dos nomes de testes é fundamental, pois torna o código mais compreensível. Quando você utiliza nomes claros e descritivos para os testes, qualquer pessoa que os analise consegue entender rapidamente o que está sendo verificado. Isso facilita a vida dos desenvolvedores, permitindo que identifiquem rapidamente o foco de cada teste.

Além disso, nomes bem escolhidos funcionam como uma forma de documentação, explicando o que o código deve fazer sem a necessidade de comentários extras. Quando um teste falha, um nome descritivo ajuda a identificar rapidamente a causa do problema, economizando tempo na correção de erros.

Escrevendo os testes

Após criar a classe de testes, começamos com a anotação @ExtendWith(MockitoExtension.class), que informa ao JUnit que queremos habilitar as funcionalidades do Mockito para os testes dessa classe. Essa anotação permite utilizar recursos como a injeção automática de mocks, ou seja, o Mockito criará automaticamente os objetos simulados necessários, simplificando o código do teste.

import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MockitoExtension.class)
public class ExerciseServiceImplTest {
}

Vamos testar a classe ExerciseServiceImpl, então precisamos fazer a injeção de dependências dessa classe.

import org.mockito.InjectMocks;
import com.leogym.gym_manager.service.serviceImpl.ExerciseServiceImpl;

@InjectMocks
private ExerciseServiceImpl exerciseService;

Como a função findExerciseById chama o método findById disponível no repositório de exercícios, vamos mockar essa função do repositório.

import org.mockito.Mock;
import com.leogym.gym_manager.repository.ExerciseRepository;

@Mock
private ExerciseRepository exerciseRepository;

Após configurar o mock, podemos começar a escrever o teste. Vamos nomear o teste como findExerciseById_ShouldReturnExercise_WhenIdExists, pois esse nome deixa claro que estamos verificando se o método findExerciseById retorna um exercício quando um ID válido é fornecido. Para indicar que essa função é um teste, colocamos a anotação @Test logo acima da declaração do método.

import org.junit.jupiter.api.Test;

@Test
void findById_ShouldReturnExercise_WhenIdExists () {
}

Agora, vamos simular as entidades para não precisarmos usar dados reais durante o teste, no caso aqui, só temos uma, mas poderíamos simular varias.

Exercise exerciseEntity = new Exercise();
exerciseEntity.setId(1L);
exerciseEntity.setName("Test Exercise");

Com as entidades necessárias simuladas, vamos configurar nosso teste para que, sempre que uma função chamar algo do repositório, ela retorne essa entidade simulada. Para isso, utilizamos uma função do Mockito chamada when. Essa função nos permite definir como um mock deve se comportar, especificando que, quando um determinado método for chamado, ele retornará o que definimos.

Para configurar o mock do repositório, vamos fazer com que, quando o método findById do exerciseRepository for chamado com o ID 1L, ele retorne a entidade exerciseEntity que já inicializamos. Essa configuração garante que nosso teste funcione de maneira controlada e previsível, utilizando dados simulados em vez de dados reais.

import static org.mockito.Mockito.when;

when(exerciseRepository.findById(1L)).thenReturn(Optional.of(exerciseEntity));

Após configurar o mock do repositório, podemos chamar a função do serviço que estamos testando. Neste caso, vamos invocar o método findExerciseById do exerciseService:

ExerciseDTO exerciseDTO = exerciseService.findExerciseById(1L);

Por fim, escreveremos o código do teste para verificar se o nome do exercício retornado é o que esperamos. Utilizamos a seguinte estrutura de asserção:

assertEquals("Test Exercise",exerciseDTO.getName() );

Aqui, assertEquals(expected, actual) é a estrutura que usamos: expected é o valor que você espera receber do objeto, enquanto actual é o valor retornado pelo objeto que estamos testando. Essa abordagem ajuda a garantir que a lógica do nosso serviço esteja funcionando corretamente.

image

Se você tiver dúvidas sobre se o método realmente está retornando o objeto correto do repositório, pode alterar o valor que espera receber. Ao fazer isso, a mensagem de erro gerada em caso de falha fornecerá os detalhes sobre o que aconteceu:

image

Neste exemplo, a mensagem de erro indica que esperávamos o nome "Nome erro", mas o método retornou "Test Exercise". Isso permite que você visualize rapidamente a discrepância entre o que foi esperado e o que realmente foi recebido, facilitando a identificação de problemas no código.

Teste de Exception

Para testar uma exceção, a estrutura é semelhante, primeiro daremos um nome adequado ao teste:

@Test
void findById_ShouldReturnNotFoundException() {
}

O retorno do repositório precisa ser um objeto vazio, oque indica que não existe um exercício com aquele ID:

when(exerciseRepository.findById(1L)).thenReturn(Optional.empty());

Dessa vez, esperamos que a chamada da função lance a exceção em vez de retornar um resultado, em vez de apenas chamar a função e armazenar o resultado em um objeto, utilizamos o método assertThrows. Com isso, podemos armazenar a resposta na variável da exceção. Por exemplo:

NotFoundException exception = assertThrows(NotFoundException.class, () -> {
  exerciseService.findExerciseById(1L); });

Neste código, assertThrows verifica se a exceção NotFoundException é lançada quando chamamos o método findExerciseById com um ID inválido. Se a exceção for lançada conforme esperado, o teste passa; caso contrário, falhará, indicando que a exceção não foi gerada como esperado.

A mensagem de retorno tem que ser igual a esperada, oque nos deixaria com: 

assertEquals("Exercício não encontrado com o id: " + 1L, exception.getMessage() );

Assim, completamos o teste de exception:

image

Para verificar se a mensagem da exceção está correta, podemos definir um valor esperado diferente da mensagem que está no código do serviço. Por exemplo, podemos usar uma mensagem como "Exercício não encontrado com o id: " ao invés de especificar qual ID não foi encontrado. Dessa forma, podemos testar se a mensagem lançada pela exceção corresponde ao que esperamos. Isso é útil para garantir que o código de tratamento de erros esteja funcionando como deveria, além de fornecer uma validação adicional para a lógica do serviço.

image

E para fechar, a ultima exception.

image

Com isso, concluímos a criação dos testes para nossa função de busca de exercícios por ID. Testar tanto os casos de sucesso quanto os casos de erro é fundamental, pois garante que o código do serviço funcione corretamente, mesmo quando algo inesperado acontece. Essa prática ajuda a evitar problemas quando a aplicação estiver em uso e torna a manutenção do código mais fácil. Assim, se fizermos alterações no futuro, os testes automatizados nos avisarão rapidamente se algo não funcionar como deveria. O objetivo é testar tudo o que for possível, cobrindo cada cenário relevante na função, para assegurar que nosso código esteja sempre em bom funcionamento.

Referencias

Como utiliza Junit, Mockito e Spring, Rodolfo Ramos: <https://rdrblog.com.br/java/como-utiliza-junit-mockito-e-spring/#:~:text=Mockito%20%C3%A9%20uma%20biblioteca%20de,em%20um%20ambiente%20de%20teste.>

Unit Test Naming Conventions, Oleksandr Stefanovskyi: <https://medium.com/@stefanovskyi/unit-test-naming-conventions-dd9208eadbea>

Compartilhe
Comentários (0)