Testes em Java: de unitários a testes de ponta a ponta
A qualidade do código é um dos principais indicadores de profissionalismo de um desenvolvedor. No mundo Java, os testes há muito deixaram de ser apenas uma boa prática e se tornaram um padrão da indústria. Sem testes confiáveis, é difícil manter grandes projetos, implementar novos recursos e refatorar o código com confiança. Neste artigo, vamos analisar três níveis principais de teste: unit (unitário), integration (integração) e e2e (ponta a ponta). Você aprenderá como eles diferem, como escrevê-los corretamente em Java e quais ferramentas usar.
A pirâmide de testes e por que ela é importante
Antes de nos aprofundarmos nos detalhes, vale a pena lembrar o conceito da pirâmide de testes (Mike Cohn). Ela visualiza a proporção ideal de testes em diferentes níveis:
- Testes unitários (base da pirâmide) — os mais rápidos, baratos e numerosos. Devem representar cerca de 70%.
- Testes de integração (meio) — verificam a interação entre componentes. Cerca de 20%.
- Testes E2E (topo) — os mais lentos e caros, mas criticamente importantes para verificar cenários de usuário. Cerca de 10%.
Seguir essa proporção permite obter o máximo de confiança no código com o mínimo de tempo gasto na execução dos testes.
1. Testes unitários em Java
Testes unitários verificam isoladamente uma "unidade" de código — geralmente um método ou classe. Seu principal objetivo é garantir que a lógica do método funcione corretamente, independentemente de dependências externas (banco de dados, API, sistema de arquivos). Para o isolamento, são usados mock objects.
Ferramentas principais
- JUnit 5 — framework padrão para escrever testes.
- Mockito — biblioteca para criar mock objects e verificar interações.
- AssertJ — biblioteca para asserções fluidas (aumenta a legibilidade).
Exemplo de teste unitário com JUnit 5 e Mockito
Suponha que temos um serviço UserService que depende de UserRepository:
public class UserService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } public User findById(Long id) { return repository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found")); }}Teste para este método:
import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.junit.jupiter.MockitoExtension;import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)class UserServiceTest {
@Mock private UserRepository repository;
@InjectMocks private UserService userService;
@Test void testFindById_UserExists() { // Given Long userId = 1L; User expectedUser = new User(userId, "Alice"); when(repository.findById(userId)).thenReturn(Optional.of(expectedUser));
// When User actualUser = userService.findById(userId);
// Then assertNotNull(actualUser); assertEquals("Alice", actualUser.getName()); verify(repository).findById(userId); }
@Test void testFindById_UserNotFound() { // Given Long userId = 99L; when(repository.findById(userId)).thenReturn(Optional.empty());
// When & Then assertThrows(UserNotFoundException.class, () -> userService.findById(userId)); }}Best practices:
- Teste apenas u