Las pruebas unitarias son una práctica fundamental en el desarrollo de software que garantiza la confiabilidad y corrección de su código. En este artículo, exploraremos los conceptos clave de las pruebas unitarias de Java, incluidos ejemplos de código simples, parametrización, pruebas de excepciones, anotaciones como @Before
, @BeforeEach
, @After
y @AfterEach
, así como el uso de simulacros y apéndices. .
También presentaremos la popular biblioteca Mockito para burlarnos y brindaremos orientación sobre cómo medir la cobertura del código utilizando complementos de Maven.
Las pruebas unitarias son la práctica de probar componentes individuales o unidades de código de forma aislada para verificar su corrección. El objetivo principal es detectar y corregir errores en las primeras etapas del proceso de desarrollo, garantizando que cada unidad de código se comporte como se espera.
Comencemos creando una clase de calculadora simple que podamos usar para nuestros ejemplos. La clase Calculator
contiene operaciones 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 crear una prueba unitaria para nuestra clase Calculator
, puede utilizar el marco JUnit, un marco de prueba popular para Java. Escribamos una prueba unitaria simple para el 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); } }
En esta prueba, importamos las clases JUnit necesarias y anotamos el método de prueba con @Test
. Luego creamos una instancia de la clase Calculator
y afirmamos que el resultado del método add
es igual al valor esperado (5).
Las pruebas parametrizadas le permiten ejecutar la misma lógica de prueba con múltiples conjuntos de datos de entrada. Esto es útil para probar un método con varios valores de entrada. Para hacer esto en JUnit, puede usar la anotación @ParameterizedTest
y proporcionar los valores de entrada y los resultados esperados como parámetros. He aquí un ejemplo:
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); } }
En este ejemplo, la anotación @ParameterizedTest
nos permite proporcionar múltiples conjuntos de valores de entrada y resultados esperados en formato CSV. El método de prueba se ejecuta para cada combinación, lo que garantiza la corrección del método add
.
Probar las excepciones es fundamental para garantizar que su código maneje los errores de manera adecuada. Para probar casos de excepción, puede utilizar la anotación @Test
junto con el método assertThrows
proporcionado por JUnit.
He aquí un ejemplo:
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)); } }
En esta prueba, verificamos que dividir un número entre cero en la clase Calculator
arroja una ArithmeticException
.
Anotaciones como @Before
, @BeforeEach
, @After
y @AfterEach
se utilizan para configurar y desmantelar el entorno de prueba. Estas anotaciones ayudan a gestionar las tareas comunes de inicialización y limpieza de pruebas.
@Before
y @After
se ejecutan una vez antes y después de todos los métodos de prueba en la clase de prueba, respectivamente.@BeforeEach
y @AfterEach
se ejecutan antes y después de cada método de prueba.
He aquí un ejemplo:
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); } }
En este ejemplo, el método setUp
está anotado con @BeforeEach
e inicializa el objeto Calculator
antes de cada prueba. El método tearDown
, anotado con @AfterEach
, limpia los recursos después de cada prueba.
Los simulacros y los resguardos son esenciales en las pruebas unitarias cuando se desea aislar el código que se está probando de dependencias externas, como bases de datos o servicios web. Le permiten simular el comportamiento de estas dependencias.
Ampliemos la clase Calculator
para incluir un método que involucre una dependencia externa y luego creemos un código auxiliar para simular esa dependencia.
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(); } }
En esta clase Calculator
actualizada, performComplexCalculation
se basa en un ExternalService
para multiplicar y obtener un valor constante.
A continuación se explica cómo puede crear un código auxiliar para probar la clase Calculator
sin depender del 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 } }
En esta prueba, ExternalService
se incluye dentro del método de prueba mediante la creación de una clase anónima que anula los métodos necesarios. De esta manera, el método de prueba Calculator
se ejecuta sin depender de la implementación real de ExternalService
.
Los stubs son útiles para simular el comportamiento de sistemas externos o dependencias para aislar y probar la funcionalidad específica de una clase o método. Esto le permite controlar el comportamiento del stub y centrarse en la unidad bajo prueba sin la necesidad de servicios externos reales.
Mockito es una biblioteca Java popular para crear y administrar objetos simulados. Digamos que tenemos una clase PaymentService
que interactúa con una pasarela de pago externa. Podemos usar Mockito para crear un simulacro de la pasarela de pago:
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); } }
En este ejemplo, creamos un PaymentGateway
simulado y usamos el método when
para definir su comportamiento. Luego llamamos al método processPayment
en la clase PaymentService
y verificamos que el método charge
se llamó con los parámetros esperados.
La cobertura de código mide el porcentaje de líneas de código, ramas o declaraciones que ejecutan sus pruebas unitarias. Le ayuda a identificar código no probado y áreas que pueden requerir pruebas adicionales.
Maven es una herramienta de compilación popular para proyectos Java y puede integrar el análisis de cobertura de código en su proyecto Maven utilizando complementos como JaCoCo. Aquí le mostramos cómo agregar JaCoCo a su proyecto:
pom.xml
de su proyecto. <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>
Ejecute su compilación Maven con el siguiente comando:
mvn clean verify
Después de ejecutar la compilación, puede encontrar el informe de cobertura del código en el archivo target/site/jacoco/index.html
. Este informe proporciona información detallada sobre la cobertura de su código por sus pruebas unitarias.
Las pruebas unitarias de Java son una práctica esencial para garantizar la confiabilidad y corrección de su código. Con herramientas como JUnit y Mockito, puede escribir pruebas unitarias y simulacros efectivos para sus componentes.
Al integrar el análisis de cobertura de código con Maven y JaCoCo, puede asegurarse de que sus pruebas cubran una parte importante de su código base. Probar y mantener pruebas unitarias periódicamente le ayudará a producir aplicaciones Java robustas y de alta calidad.
En la siguiente parte de esta serie, exploraremos las pruebas de integración, un aspecto crítico de las pruebas de software que implica probar las interacciones entre diferentes componentes y servicios para garantizar que el sistema general funcione correctamente.
¡Estén atentos a la segunda parte, donde nos sumergiremos en el apasionante mundo de las pruebas de integración!