O teste unitário é uma prática fundamental no desenvolvimento de software que garante a confiabilidade e correção do seu código. Neste artigo, exploraremos os principais conceitos de teste de unidade Java, incluindo exemplos de código simples, parametrização, teste de exceção, anotações como @Before
, @BeforeEach
, @After
e @AfterEach
, bem como o uso de mocks e stubs .
Também apresentaremos a popular biblioteca Mockito para simulação e forneceremos orientação sobre como medir a cobertura de código usando plug-ins Maven.
O teste de unidade é a prática de testar componentes individuais ou unidades de código isoladamente para verificar sua correção. O objetivo principal é detectar e corrigir bugs no início do processo de desenvolvimento, garantindo que cada unidade de código se comporte conforme o esperado.
Vamos começar criando uma classe de calculadora simples que podemos usar em nossos exemplos. A classe Calculator
contém operações aritméticas básicas:
public class Calculator { public int add(int a, int b) { return a + b; } public int subtract(int a, int b) { return a - b; } public int multiply(int a, int b) { return a * b; } public int divide(int a, int b) { return a / b; } }
Para criar um teste de unidade para nossa classe Calculator
, você pode usar a estrutura JUnit, uma estrutura de teste popular para Java. Vamos escrever um teste de unidade simples para o método add
:
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculatorTest { @Test public void testAdd() { Calculator calculator = new Calculator(); int result = calculator.add(2, 3); assertEquals(5, result); } }
Neste teste, importamos as classes JUnit necessárias e anotamos o método de teste com @Test
. Em seguida, criamos uma instância da classe Calculator
e afirmamos que o resultado do método add
é igual ao valor esperado (5).
Os testes parametrizados permitem executar a mesma lógica de teste com vários conjuntos de dados de entrada. Isto é útil para testar um método com vários valores de entrada. Para fazer isso no JUnit, você pode usar a anotação @ParameterizedTest
e fornecer os valores de entrada e os resultados esperados como parâmetros. Aqui está um exemplo:
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculatorTest { @ParameterizedTest @CsvSource({"2, 3, 5", "4, 7, 11", "0, 0, 0"}) public void testAdd(int a, int b, int expected) { Calculator calculator = new Calculator(); int result = calculator.add(a, b); assertEquals(expected, result); } }
Neste exemplo, a anotação @ParameterizedTest
nos permite fornecer vários conjuntos de valores de entrada e resultados esperados em formato CSV. O método de teste é executado para cada combinação, garantindo a correção do método add
.
Testar exceções é crucial para garantir que seu código lide com os erros de maneira adequada. Para testar casos de exceção, você pode usar a anotação @Test
junto com o método assertThrows
fornecido pelo JUnit.
Aqui está um exemplo:
javaCopy codeimport org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; public class CalculatorTest { @Test public void testDivideByZero() { Calculator calculator = new Calculator(); assertThrows(ArithmeticException.class, () -> calculator.divide(5, 0)); } }
Neste teste, verificamos que dividir um número por zero na classe Calculator
gera uma ArithmeticException
.
Anotações como @Before
, @BeforeEach
, @After
e @AfterEach
são usadas para configurar e desmontar o ambiente de teste. Essas anotações ajudam no gerenciamento de tarefas comuns de inicialização e limpeza de testes.
@Before
e @After
são executados uma vez antes e depois de todos os métodos de teste na classe de teste, respectivamente.@BeforeEach
e @AfterEach
são executados antes e depois de cada método de teste.
Aqui está um exemplo:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculatorTest { private Calculator calculator; @BeforeEach public void setUp() { calculator = new Calculator(); } @AfterEach public void tearDown() { calculator = null; } @Test public void testAdd() { int result = calculator.add(2, 3); assertEquals(5, result); } @Test public void testSubtract() { int result = calculator.subtract(5, 3); assertEquals(2, result); } }
Neste exemplo, o método setUp
é anotado com @BeforeEach
e inicializa o objeto Calculator
antes de cada teste. O método tearDown
, anotado com @AfterEach
, limpa recursos após cada teste.
Mocks e stubs são essenciais em testes unitários quando você deseja isolar o código que está sendo testado de dependências externas, como bancos de dados ou serviços web. Eles permitem simular o comportamento dessas dependências.
Vamos expandir a classe Calculator
para incluir um método que envolve uma dependência externa e então criar um stub para simular essa dependência.
public class Calculator { private ExternalService externalService; public Calculator(ExternalService externalService) { this.externalService = externalService; } public int performComplexCalculation(int a, int b) { int result = externalService.multiply(a, b); return result + externalService.getConstant(); } }
Nesta classe Calculator
atualizada, performComplexCalculation
depende de um ExternalService
para multiplicação e obtenção de um valor constante.
Veja como você pode criar um stub para testar a classe Calculator
sem depender do ExternalService
real.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; public class CalculatorTest { @Test public void testPerformComplexCalculation() { // Create a stub for ExternalService ExternalService externalServiceStub = new ExternalService() { @Override public int multiply(int a, int b) { return a * b; } @Override public int getConstant() { return 5; // Stubbed constant value } }; // Create the Calculator with the stubbed ExternalService Calculator calculator = new Calculator(externalServiceStub); // Test the performComplexCalculation method int result = calculator.performComplexCalculation(2, 3); assertEquals(11, result); // 2*3 + 5 (constant) = 11 } }
Neste teste, ExternalService
é inserido no método de teste criando uma classe anônima que substitui os métodos necessários. Dessa forma, o método de teste Calculator
é executado sem depender da implementação real de ExternalService
.
Stubs são úteis para simular o comportamento de sistemas externos ou dependências para isolar e testar a funcionalidade específica de uma classe ou método. Isso permite controlar o comportamento do stub e focar na unidade em teste sem a necessidade de serviços externos reais.
Mockito é uma biblioteca Java popular para criar e gerenciar objetos simulados. Digamos que temos uma classe PaymentService
que interage com um gateway de pagamento externo. Podemos usar o Mockito para criar uma simulação para o gateway de pagamento:
import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; public class PaymentServiceTest { @Test public void testProcessPayment() { PaymentGateway paymentGateway = mock(PaymentGateway.class); PaymentService paymentService = new PaymentService(paymentGateway); when(paymentGateway.charge(100.0)).thenReturn(true); boolean result = paymentService.processPayment(100.0); assertTrue(result); verify(paymentGateway, times(1)).charge(100.0); } }
Neste exemplo, criamos um PaymentGateway
simulado e usamos o método when
para definir seu comportamento. Em seguida, chamamos o método processPayment
na classe PaymentService
e verificamos se o método charge
foi chamado com os parâmetros esperados.
A cobertura de código mede a porcentagem de linhas de código, ramificações ou instruções executadas pelos testes de unidade. Ajuda a identificar códigos não testados e áreas que podem exigir testes adicionais.
Maven é uma ferramenta de construção popular para projetos Java, e você pode integrar a análise de cobertura de código em seu projeto Maven usando plug-ins como JaCoCo. Veja como adicionar JaCoCo ao seu projeto:
pom.xml
do seu projeto. <build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.7</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Execute sua compilação do Maven com o seguinte comando:
mvn clean verify
Depois de executar a compilação, você pode encontrar o relatório de cobertura de código no arquivo target/site/jacoco/index.html
. Este relatório fornece informações detalhadas sobre a cobertura do seu código pelos testes de unidade.
O teste de unidade Java é uma prática essencial para garantir a confiabilidade e a correção do seu código. Com ferramentas como JUnit e Mockito, você pode escrever testes de unidade e simulações eficazes para seus componentes.
Ao integrar a análise de cobertura de código com Maven e JaCoCo, você pode garantir que seus testes cubram uma parte significativa de sua base de código. Testar e manter testes de unidade regularmente o ajudará a produzir aplicativos Java robustos e de alta qualidade.
Na próxima parte desta série, exploraremos os testes de integração, um aspecto crítico dos testes de software que envolve testar as interações entre diferentes componentes e serviços para garantir que o sistema geral funcione corretamente.
Fique ligado na segunda parte, onde mergulharemos no emocionante mundo dos testes de integração!