Testes em Java: Unit, Integration, E2E — Nível Avançado
No desenvolvimento Java moderno, testar não é apenas uma formalidade, mas uma prática fundamental que garante a estabilidade e previsibilidade do código. Testes básicos, como simples @Test com Assert.assertEquals, há muito deram lugar a estratégias complexas de múltiplas camadas. Neste artigo, exploraremos técnicas avançadas para cada nível da pirâmide de testes: Unit, Integration e End-to-End (E2E). Você aprenderá a escrever testes sustentáveis, rápidos e confiáveis que transformarão seu código em um mecanismo bem ajustado.
1. Testes Unit Avançados: Mocks, Stubs e Parametrização
Testes Unit verificam um pedaço isolado de código (geralmente uma classe ou método). Em nível avançado, vamos além de cenários simples para interações complexas e casos limite. A ferramenta chave aqui é a biblioteca Mockito.
1.1. Uso de ArgumentCaptor e respostas complexas (Answer)
Frequentemente, é necessário não apenas verificar se um método foi chamado, mas quais argumentos foram passados para ele. Para isso, usamos ArgumentCaptor.
// Exemplo: Verificar se a mensagem correta foi enviada ao serviço@ExtendWith(MockitoExtension.class)public class NotificationServiceTest { @Mock private EmailSender emailSender; @InjectMocks private NotificationService notificationService;
@Test void shouldSendEmailWithCorrectContent() { // Arrange User user = new User("test@example.com", "Test User"); // Act notificationService.sendWelcomeEmail(user); // Assert ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class); verify(emailSender).send(captor.capture()); EmailMessage capturedMessage = captor.getValue(); assertEquals("test@example.com", capturedMessage.getTo()); assertTrue(capturedMessage.getBody().contains("Test User")); }}Para criar respostas dinâmicas com base nos dados de entrada, use a interface Answer:
@Testvoid testDynamicResponse() { when(dataRepository.findById(anyLong())).thenAnswer(invocation -> { Long id = invocation.getArgument(0); return new DataEntity(id, "Entity with ID: " + id); }); assertEquals("Entity with ID: 5", dataService.getDataById(5L).getName());}1.2. Testes Parametrizados (JUnit 5)
A parametrização permite executar o mesmo teste com diferentes conjuntos de dados, evitando duplicação de código. O JUnit 5 oferece anotações poderosas, como @CsvSource e @MethodSource.
@ParameterizedTest@CsvSource({ "10, 20, 30", "0, 0, 0", "-5, 5, 0"})void testSum(int a, int b, int expectedSum) { Calculator calculator = new Calculator(); assertEquals(expectedSum, calculator.add(a, b));}2. Testes de Integração: Trabalhando com Banco de Dados e Serviços Externos
Testes de integração verificam a interação entre componentes do sistema, por exemplo, com um banco de dados real ou filas. O principal desafio é gerenciar o estado do ambiente. É aqui que o Testcontainers entra em ação.
2.1. Testcontainers para Banco de Dados Isolado
O Testcontainers sobe contêineres Docker com o software necessário (PostgreSQL, Redis, Kafka) durante os testes, garantindo limpeza e reprodutibilidade.
@SpringBootTest@Testcontainersclass UserRepositoryIntegrationTest {
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15") .withDatabaseName("testdb") .withUsername("test") .withPassword("test");
@DynamicPropertySource static void configureProperties(DynamicPropertyR