Saltar al contenido principal

Testing

Hasta ahora hemos aprendido a crear programas utilizando clases, objetos, herencia, excepciones, ficheros e hilos.

A medida que nuestros programas crecen, también aumenta la posibilidad de cometer errores.

Por ejemplo, podemos tener una clase que calcula el precio final de un producto:

public class Producto {

private double precio;
private double descuento;

public Producto(double precio, double descuento) {
this.precio = precio;
this.descuento = descuento;
}

public double precioFinal() {
return precio - (precio * descuento / 100);
}

}

Podríamos comprobar manualmente que funciona correctamente creando un objeto:

Producto producto = new Producto(100, 20);

System.out.println(producto.precioFinal());

Y comprobar que obtenemos:

80.0

El problema es que, cuando tenemos muchas clases y muchos métodos, comprobar manualmente cada uno de ellos puede resultar complicado.

Además, cada vez que modificamos nuestro código tendríamos que volver a realizar todas estas comprobaciones.

Para solucionar este problema podemos utilizar tests.

Un test permite comprobar automáticamente que una parte de nuestro programa funciona como esperamos.


¿Qué es JUnit?

JUnit es una biblioteca de Java que permite crear y ejecutar tests automáticamente.

Con JUnit podemos escribir código que compruebe cosas como:

¿El método devuelve el resultado correcto?

¿Una lista contiene un determinado elemento?

¿Un método devuelve null?

¿Se produce una determinada excepción?

Por ejemplo, podemos crear un test que compruebe que:

100 € con un 20% de descuento = 80

En lugar de comprobarlo nosotros visualmente, podemos pedirle a JUnit que lo compruebe.

La idea general es:

Código de nuestra aplicación

Test

JUnit comprueba

¿Resultado correcto?
↙ ↘
Sí No
↓ ↓
Test OK Test falla

Esto permite detectar errores rápidamente.


Crear un test

Para crear un test con JUnit utilizamos la anotación:

@Test

Una anotación es una indicación que le damos a Java o a una biblioteca para proporcionar información adicional sobre nuestro código.

Por ejemplo:

@Test
public void comprobarPrecio() {

}

La anotación:

@Test

indica a JUnit que este método es un test que debe ejecutarse.


@Test

Vamos a utilizar la clase Producto que hemos visto anteriormente:

public class Producto {

private double precio;
private double descuento;

public Producto(double precio, double descuento) {
this.precio = precio;
this.descuento = descuento;
}

public double precioFinal() {
return precio - (precio * descuento / 100);
}

}

Podemos crear una clase para realizar los tests con un método que se encargue de todas las comprobaciones:

import org.junit.jupiter.api.Test;

public class ProductoTest {

// Método de testeo
@Test
public void comprobarPrecioFinal() {
// Creación de un objeto de tipo Producto
Producto producto =
new Producto(100, 20);
// Ejecución del método de Producto que queremos testar
System.out.println(
producto.precioFinal()
);

}

}

JUnit ejecutará el método:

comprobarPrecioFinal()

porque tiene la anotación:

@Test

Sin embargo, simplemente imprimir el resultado no es realmente un test.

Necesitamos indicar a JUnit qué resultado esperamos.

Para ello utilizamos las assertions.


Assertions

Las assertions son comprobaciones que permiten verificar si el resultado obtenido es el que esperábamos.

JUnit proporciona diferentes métodos para realizar estas comprobaciones.

Por ejemplo:

assertEquals()

permite comprobar que dos valores son iguales.

También podemos utilizar:

assertTrue()

para comprobar que una condición es verdadera.

O:

assertFalse()

para comprobar que una condición es falsa.

Podemos imaginar una assertion como una pregunta:

¿El resultado obtenido es el esperado?



Sí → Test correcto



No → Test falla

assertEquals()

Una de las assertions más utilizadas es:

assertEquals(valorEsperado, valorObtenido)

Su objetivo es comprobar que dos valores son iguales.

Por ejemplo:

assertEquals(80, 80);

La comprobación es:

80 esperado
80 obtenido


Son iguales?


Test correcto

Si hacemos:

assertEquals(80, 70);

la comprobación falla porque los valores son diferentes.


Utilizar assertEquals() en un test

Podemos utilizarlo para comprobar nuestro Producto:

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class ProductoTest {

@Test
public void comprobarPrecioFinal() {
// Crear el producto
Producto producto =
new Producto(100, 20);
// Guardar el resultado del método
double resultado =
producto.precioFinal();
// Comprobar si es el resultado esperado
assertEquals(80, resultado);

}

}

Aquí tenemos tres partes importantes:

double resultado =
producto.precioFinal();

Primero ejecutamos el código que queremos comprobar.

Después:

assertEquals(80, resultado);

le decimos a JUnit:

Espero que el resultado sea 80.

Si resultado vale 80, el test pasa.

Si vale cualquier otro valor, el test falla.


El test no modifica el código original

Es importante entender que un test no sustituye al código que queremos probar.

Tenemos:

Producto.java

Código de nuestra aplicación

Y por separado:

ProductoTest.java

Código encargado de comprobar Producto

El test utiliza nuestra clase, pero no forma parte de su funcionamiento.

Esto permite mantener separados:

Código de la aplicación

y:

Código de pruebas

Otras assertions

JUnit proporciona diferentes assertions dependiendo de lo que queramos comprobar.

Algunas de las más habituales son:

assertEquals()
Comprueba que dos valores sean iguales.

assertNotEquals()
Comprueba que dos valores sean diferentes.

assertTrue()
Comprueba que una condición sea verdadera.

assertFalse()
Comprueba que una condición sea falsa.

assertNull()
Comprueba que un valor sea null.

assertNotNull()
Comprueba que un valor no sea null.

Por ejemplo:

assertEquals(10, resultado);

comprueba una igualdad.

Mientras que:

assertTrue(resultado > 0);

comprueba una condición.

Y:

assertNotNull(producto);

comprueba que el objeto exista y no sea null.


assertTrue()

assertTrue() permite comprobar que una condición sea verdadera.

Por ejemplo:

assertTrue(10 > 5);

Como:

10 > 5

es verdadero, el test pasa.

Podemos utilizarlo con métodos de nuestras clases.

Por ejemplo, imaginemos una clase:

public class Persona {

private int edad;

public Persona(int edad) {
this.edad = edad;
}

public boolean esMayorDeEdad() {
return edad >= 18;
}

}

Podemos comprobarla:

@Test
public void comprobarMayorDeEdad() {

Persona persona = new Persona(20);

assertTrue(persona.esMayorDeEdad());

}

Estamos comprobando que una persona de 20 años sea considerada mayor de edad.


assertFalse()

assertFalse() funciona de forma contraria.

Comprueba que una condición sea falsa.

Por ejemplo:

assertFalse(5 > 10);

La condición:

5 > 10

es falsa, por lo que el test pasa.

Podemos utilizarlo con el ejemplo anterior:

@Test
public void comprobarMenorDeEdad() {

Persona persona = new Persona(15);

assertFalse(persona.esMayorDeEdad());

}

Aquí esperamos que:

persona.esMayorDeEdad()

devuelva:

false

assertNull() y assertNotNull()

También podemos comprobar si una variable contiene null.

Por ejemplo:

String nombre = null;

Podemos comprobarlo:

assertNull(nombre);

La comprobación será correcta porque nombre contiene null.

Para comprobar lo contrario:

String nombre = "Ana";

assertNotNull(nombre);

Aquí estamos comprobando que nombre tenga algún valor.

Estas assertions son especialmente útiles cuando nuestros métodos pueden devolver objetos que podrían ser null.


Organizar varios tests

Una clase de test puede contener varios métodos marcados con @Test.

Por ejemplo:

public class PersonaTest {

@Test
public void comprobarMayorDeEdad() {

Persona persona =
new Persona(20);

assertTrue(
persona.esMayorDeEdad()
);

}

@Test
public void comprobarMenorDeEdad() {

Persona persona =
new Persona(15);

assertFalse(
persona.esMayorDeEdad()
);

}

}

Cada método representa una comprobación diferente.

Podemos tener:

PersonaTest

├── comprobarMayorDeEdad()

└── comprobarMenorDeEdad()

JUnit ejecutará ambos tests por separado.

Esto permite detectar de forma más precisa qué parte del código está funcionando y cuál tiene un problema.


@BeforeEach

Cuando tenemos varios tests, puede ocurrir que necesitemos preparar algunos objetos o datos antes de cada uno de ellos.

Por ejemplo, imaginemos que tenemos:

Producto producto;

y queremos utilizarlo en varios tests.

Podríamos crear el producto dentro de cada test:

@Test
public void comprobarPrecio() {

Producto producto =
new Producto(100, 20);

// Test...

}

@Test
public void comprobarOtroCaso() {

Producto producto =
new Producto(200, 10);

// Test...

}

Pero si necesitamos realizar siempre una determinada preparación antes de cada test, podemos utilizar:

@BeforeEach

Esta anotación indica a JUnit:

Ejecuta este método antes de cada test.


Ejemplo de @BeforeEach

Podemos crear:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class ProductoTest {

private Producto producto;

@BeforeEach
public void preparar() {

producto =
new Producto(100, 20);

}

@Test
public void comprobarPrecioFinal() {

assertEquals(
80,
producto.precioFinal()
);

}

}

Antes de ejecutar:

comprobarPrecioFinal()

JUnit ejecutará:

preparar()

Podemos visualizarlo:

@BeforeEach

preparar()

@Test

comprobarPrecioFinal()

Si tenemos varios tests:

@BeforeEach

@Test 1

@BeforeEach

@Test 2

@BeforeEach

@Test 3

El método marcado con @BeforeEach se ejecuta antes de cada test, no solamente una vez.


¿Para qué sirve @BeforeEach?

Es especialmente útil cuando varios tests necesitan una preparación común.

Por ejemplo:

Crear objetos

Preparar datos

Inicializar variables

Configurar una situación inicial

En lugar de repetir esa preparación:

@Test
public void test1() {
// preparar...
// comprobar...
}

@Test
public void test2() {
// preparar...
// comprobar...
}

@Test
public void test3() {
// preparar...
// comprobar...
}

podemos colocarla en:

@BeforeEach
public void preparar() {

// preparación común

}

De esta manera nuestros tests son más fáciles de mantener.


@AfterEach

De la misma forma que podemos ejecutar código antes de cada test, también podemos ejecutar código después de cada test.

Para ello utilizamos:

@AfterEach

Esta anotación indica:

Ejecuta este método después de cada test.

Por ejemplo:

@AfterEach
public void terminar() {

System.out.println("Test terminado");

}

Si tenemos:

@BeforeEach
public void preparar() {

System.out.println("Preparando");

}

@Test
public void testUno() {

System.out.println("Ejecutando test");

}

@AfterEach
public void terminar() {

System.out.println("Terminando");

}

el orden será:

Preparando

Ejecutando test

Terminando

¿Para qué sirve @AfterEach?

@AfterEach se utiliza para realizar tareas de limpieza después de cada test.

Por ejemplo, si durante un test:

Creamos un archivo

Abrimos un recurso

Modificamos algún dato

Creamos una conexión

podemos necesitar dejar todo preparado para el siguiente test.

En esos casos podemos utilizar @AfterEach.

Por ejemplo:

@AfterEach
public void limpiar() {

System.out.println("Limpiando recursos");

}

La idea general es:

@BeforeEach

Preparar

@Test

Comprobar

@AfterEach

Limpiar

@BeforeEach y @AfterEach juntos

Podemos utilizar ambas anotaciones en una misma clase.

Por ejemplo:

public class ProductoTest {

private Producto producto;

@BeforeEach
public void preparar() {

producto =
new Producto(100, 20);

}

@Test
public void comprobarPrecio() {

assertEquals(
80,
producto.precioFinal()
);

}

@AfterEach
public void limpiar() {

producto = null;

}

}

El ciclo de ejecución sería:

@BeforeEach

Crear Producto

@Test

Comprobar resultado

@AfterEach

Limpiar

Si tenemos varios tests, este ciclo se repite para cada uno:

@BeforeEach → @Test → @AfterEach

@BeforeEach → @Test → @AfterEach

@BeforeEach → @Test → @AfterEach

Ejemplo completo

Vamos a juntar los conceptos principales en una clase de test.

Tenemos la siguiente clase:

public class Calculadora {

public int sumar(int a, int b) {

return a + b;

}

public int restar(int a, int b) {

return a - b;

}

public boolean esPositivo(int numero) {

return numero > 0;

}

}

Podemos crear:

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class CalculadoraTest {

private Calculadora calculadora;

@BeforeEach
public void preparar() {

calculadora = new Calculadora();

}

@Test
public void comprobarSuma() {

int resultado =
calculadora.sumar(5, 3);

assertEquals(8, resultado);

}

@Test
public void comprobarResta() {

int resultado =
calculadora.restar(10, 4);

assertEquals(6, resultado);

}

@Test
public void comprobarNumeroPositivo() {

assertTrue(
calculadora.esPositivo(10)
);

}

@AfterEach
public void terminar() {

calculadora = null;

}

}

Tenemos tres tests:

comprobarSuma()

5 + 3 = 8


comprobarResta()

10 - 4 = 6


comprobarNumeroPositivo()

10 es positivo

Y todos utilizan el mismo ciclo:

@BeforeEach

Crear Calculadora

@Test

Realizar comprobación

@AfterEach

Limpiar

Estructura habitual de un test

Un test suele tener tres partes conceptuales:

Preparar

Ejecutar

Comprobar

Por ejemplo:

@Test
public void comprobarSuma() {

// Preparar
Calculadora calculadora =
new Calculadora();

// Ejecutar
int resultado =
calculadora.sumar(5, 3);

// Comprobar
assertEquals(8, resultado);

}

Primero preparamos los objetos necesarios.

Después ejecutamos el método que queremos probar.

Finalmente comprobamos que el resultado sea el esperado.

Cuando utilizamos @BeforeEach, parte de la preparación puede moverse fuera del test. Esto evita repetir código cuando muchos tests necesitan la misma configuración inicial.


Errores frecuentes

Olvidar @Test

Un método como:

public void comprobarSuma() {

// ...

}

no será reconocido como un test por JUnit.

Debemos utilizar:

@Test
public void comprobarSuma() {

// ...

}

Confundir assertEquals() con mostrar el resultado

Esto:

System.out.println(resultado);

simplemente muestra información por pantalla. No comprueba automáticamente que el resultado sea correcto.

En un test debemos utilizar una assertion:

assertEquals(8, resultado);

De esta forma JUnit puede determinar si el test ha pasado o ha fallado.


Utilizar un valor esperado incorrecto

Si sabemos que:

5 + 3 = 8

no tendría sentido escribir:

assertEquals(10, resultado);

El valor esperado debe representar el resultado correcto que queremos que produzca nuestro programa.


Confundir @BeforeEach con @AfterEach

Recordemos:

@BeforeEach

Antes de cada test

Mientras que:

@AfterEach

Después de cada test

Por ejemplo:

@BeforeEach
public void preparar() {

// Preparación

}

y:

@AfterEach
public void limpiar() {

// Limpieza

}

Resumen

JUnit permite crear tests automáticos para comprobar que nuestro código funciona como esperamos.

La anotación:

@Test

indica que un método es un test.

Las assertions permiten comprobar resultados.

Algunas de las más utilizadas son:

assertEquals()
assertNotEquals()
assertTrue()
assertFalse()
assertNull()
assertNotNull()

@BeforeEach permite ejecutar código antes de cada test:

@BeforeEach
public void preparar() {

// Preparación

}

@AfterEach permite ejecutar código después de cada test:

@AfterEach
public void limpiar() {

// Limpieza

}

Y la idea principal de un test:

Código de la aplicación

Test

Ejecutar método

Assertion

¿Resultado esperado?
↙ ↘
Sí No
↓ ↓
Pasa Falla

Los tests permiten detectar errores de forma automática y son especialmente importantes cuando los proyectos empiezan a crecer, ya que nos permiten comprobar que los cambios realizados en una parte del programa no han provocado errores en otras partes.