
Espero que el lector tenga asumida la importancia de las pruebas automáticas. Para llevar la teoría a la práctica, necesitamos una librería o marco de trabajo que nos permita escribirlas y ejecutarlas, ofreciendo de forma clara el resultado. En este capítulo veremos las principales características de JUnit 5 que usaremos a lo largo del curso.
Presentando JUnit
JUnit es el marco de trabajo de código abierto para la realización de pruebas con Java más popular. Según este análisis, en 2016 era la librería más exitosa, y aunque en 2019 ha caído al puesto treinta y tres, sigue siendo la primera utilidad relacionada con el testing. Entre las alternativas, es imprescindible mencionar TestNG y Spock.
A pesar de lo que sugiere su nombre, con JUnit podemos escribir pruebas de cualquier tipo y no solo unitarias. Nos proporciona la infraestructura necesaria, lo que hagan ya es cosa nuestra.
Pese a su enorme éxito y ser un estándar de facto en toda regla, mucho ha llovido desde que JUnit 4 se publicara en 2006. Si vamos más allá de las simples pruebas unitarias, nos hace falta integración, por ejemplo, con Spring o servidores como WildFly, y esa es una de las debilidades de JUnit 4: estamos ante una aplicación monolítica -un único .jar- con unas posibilidades de extensión limitadas. La integración con otros productos consiste en crear una versión específica del runner o «ejecutor de test» especializando la clase Runner o alguna de sus subclases. El problema es que una prueba solo puede ser ejecutada por un Runner.
Durante años ha sido necesario lidiar con este problema. En Android, por ejemplo, se usa el mecanismo de reglas. Esta y otras limitaciones provocaron el advenimiento de JUnit 5 cuyo desarrollo fue inicialmente financiado con una campaña de crowdfunding. El resultado es un moderno marco de trabajo modular y extensible, escrito desde cero con Java 8 y compatible con versiones previas. Como usuarios finales, lo que más nos interesa es la inclusión de funcionalidades históricamente muy demandadas y que ahora son posibles.
En esta figura vemos de forma simplificada la arquitectura de JUnit 5 y sus tres componentes principales.

- Platform. El corazón de todo el marco de trabajo, una plataforma de alto nivel para ejecutar pruebas en la máquina virtual de Java a través de librerías que implementen su «engine». También provee una API que permite la ejecución de las pruebas con herramientas externas tales como Maven, Gradle, IntelliJ, Eclipse… (JUnit 5 incluye la aplicación Console Launcher para lanzar los test).
- Jupiter. Es una librería de testing que forma parte JUnit 5. Cuando hablamos de pruebas implementadas con JUnit 5, en realidad nos referimos al uso de Jupiter. Viene cargada de útiles características y no deja de evolucionar.
- Vintage. Otra librería de testing. Sirve para ejecutar pruebas escritas con JUnit 3 y 4, así que no hay que migrarlas cuando empecemos a utilizar Jupiter en un proyecto en curso. De este modo, se facilita la adopción de la nueva plataforma.
Si bien Jupiter y Vintage forman parte del proyecto JUnit 5, de ahí que en la figura aparezcan en la misma «caja» que platform, son módulos opcionales, pues no dejan de ser dos librerías de testing desarrolladas sobre la plataforma del mismo modo que puede haber muchas otras. Las librerías de terceros y las extensiones -las veremos al final de este capítulo- se recopilan en este documento.
El primer test
Ha llegado el momento de ponerse manos a la obra con un proyecto de ejemplo. Necesitamos la dependencia de Jupiter en su pom.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
Los IDE como Eclipse e IntelliJ ya son capaces de ejecutar las pruebas. En Maven, se precisa el plugin Surefire. En principio, no es necesario declararlo porque ya está implícito, pero será la versión de Maven la que determine la versión del plugin, y JUnit 5 requiere como mínimo surefire 2.22.0. Sin la versión adecuada de surefire, no se producen errores, simplemente no se encuentran pruebas a ejecutar.
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
Lo más fiable es declarar la versión exacta de surefire.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.version}</version>
</plugin>
El ámbito o scope de tipo test indica que la librería solo se usará para la realización de pruebas. No se incluirá en el artefacto final que empaquete la aplicación.
Cada prueba se escribe en un método. Según el estándar de Maven, las clases con estos métodos estarán en la carpeta /src/test/java y sus recursos en /src/test/resources. Al igual que la dependencia, estas clases y ficheros tampoco se incluyen en el artefacto final. Una práctica habitual consiste en organizar las pruebas en paquetes según su tipología (unitario, integración, e-2-e) porque vimos en el capítulo previo que la naturaleza y, sobre todo, los tiempos de ejecución de cada categoría son muy dispares.
He aquí el primer ejemplo.
package com.danielme.jakartaee;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Comparator;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ComparatorsTest {
@Test
@DisplayName("probando naturalOrder con enteros en JUnit 5")
void testComparatorLessThan() {
Comparator integerComparator = Comparator.naturalOrder();
int result = integerComparator.compare(1, 2);
if (result < -1) {
fail("el resultado no es un número negativo. resultado: " + result);
}
}
}
La clase ComparatorsTest cumple dos requisitos.
- No es privada. Para decidir su nombre, he seguido la convención de añadir el sufijo Test al nombre de la clase sujeto de las pruebas. De forma predeterminada, Surefire requiere que las clases con pruebas verifiquen uno de estos patrones: «**/Test*.java«, «**/*Test.java», «**/*Tests.java«, «**/*TestCase.java». La documentación explica cómo cambiar esto.
- La prueba en sí misma es un método anotado con @Test. No devuelve nada y no puede ser ni privado ni estático. Si también tenemos la librería Vintage en el proyecto, no confundamos la anotación @org.junit.jupiter.api.Test con @org.junit.Test. Vintage y Jupiter comparten el nombre de algunas anotaciones, y es el típico error «tonto» que puede hacernos perder bastante tiempo.
Prestemos especial atención al nombre del método: ha de ser corto pero descriptivo. Suele empezar por test o should («debería»). Algunos programadores prefieren usar el estilo Snake Case en lugar de Camel case: las palabras se separan con un guion bajo «_» en vez de comenzar la siguiente palabra con mayúsculas. Esto posibilita nombres largos fáciles de leer. No es mi costumbre; el lector debe elegir el criterio que considere más cómodo.
Se puede usar la anotación @DisplayName en el método y/o la clase para indicar con una descripción corta (¡admite emojis!) qué hace el test. Esta información aparece en los resultados y aumenta su legibilidad.
Volvamos al código testComparatorLessThan. ¿Ves los saltos de línea? La división en tres bloques es intencionada y responde a la estructura <<Arrange-Act-Assert>> (AAA).
- Arrange (Organizar). Se configuran los elementos necesarios para realizar la prueba. Esta tarea incluye instanciar las clases a probar y preparar los objetos dobles de tests cuando sea necesario.
- Act (Actuar). Se realiza la operación a validar.
- Assert (Afirmar). Comprobamos que el resultado de la operación es el esperado. Cada comprobación se denomina aserción. Más adelante veremos como escribir buenas aserciones.
El patrón resulta simple y práctico. Nos ayuda a diseñar mejor los tests, sistematizando su creación y haciéndolos más claros. Si la prueba que estamos escribiendo realiza más de una instrucción en la fase Act, quizás esté mal enfocada, sobre todo si es de tipo unitario. Un test solo debería probar una cosa y solo puede fallar por una causa conocida de antemano.
Nota. Estos tres pasos son equivalentes a los del patrón <<Given-When-Then>>, también muy seguido a la hora de dar forma a las pruebas.
¿Cuál es el resultado de una prueba? Es fallida si invocó al método fail. Será errónea si no se ha podido completar su ejecución por un error inesperado, por ejemplo una excepción producida durante la fase Act. En el resto de casos, la prueba es exitosa.
Ejecución
Vamos a lanzar la prueba con las herramientas de desarrollo que estamos usando en el curso.
Maven
Por omisión, todos los tests del proyecto detectados por Surefire se ejecutarán en la fase test del ciclo de vida de Maven. Este es el ciclo básico.
validate -> compile -> test -> package -> verify -> install
La fase test se sitúa justo antes del empaquetado del artefacto. Dado que cuando algún test es fallido o erróneo la construcción del proyecto se aborta, el artefacto no se generará si no se verifican todos los tests. Si esto supone un problema, podemos omitir la ejecución de las pruebas con el parámetro -DskipTests.
$ mvn clean package -DskipTests
De forma explícita, las pruebas se ejecutan así (el proyecto de ejemplo incluye el wrapper de Maven como es habitual)
$ mvn test

Además de la salida de Maven en la terminal que vemos en la imagen anterior, los resultados de la ejecución se guardan en la carpeta target/surefire-reports. Hay un informe por cada clase con pruebas.

También es posible especificar las clases de prueba con el parámetro test.
$ mvn test -Dtest="ComparatosTest"
Existe otro plugin denominado failsafe pensado para ejecutar pruebas de integración y que es complementario a Surefire. Estas son sus diferencias con respecto a Surefire.
- Es obligatorio declararlo en el pom; no está implícito.
- Reconoce las pruebas de las clases cuyo nombre verifique uno de estos patrones: «**/IT*.java«, «**/*IT.java» «**/*ITCase.java». De nuevo, esto es configurable.
- Las pruebas se ejecutan en la fase verify ubicada entre package e install.
Por simplicidad, no lo usaremos en el curso, ni siquiera cuando desarrollemos pruebas de integración.
IntelliJ y Eclipse
Ambos IDE incluyen de serie de integración con Jupiter y Vintage. Las pruebas contenidas en una clase se lanzan la opción “Run” de su menú contextual. Se puede hacer lo mismo con un paquete para ejecutar todos los tests contenidos en sus clases.
Los resultados se muestran de forma bastante comprensible en la vista correspondiente (Run en IntelliJ, JUnit en Eclipse).
También se pueden ejecutar las pruebas en modo «Cobertura».
En IntelliJ.

En Eclipse.

En los dos entornos tenemos una vista especifica para los resultados. Las siguientes capturas, pertenecientes a mi tutorial sobre JUnit 4 porque en el proyecto de este capítulo no existe código sobre el que medir la cobertura, dan buena cuenta de ello.


Aserciones
Jupiter
Hay una manera más sencilla de escribir la aserción de los resultados que implementar la comprobación e invocar a fail. Jupiter incluye una clase (org.junit.jupiter.Assertions) con numerosos métodos estáticos para verificar condiciones básicas sobre objetos y tipos de datos primitivos. Destacan los siguientes.
- assertTrue\assertFalse: verifica que una condición se evalúa como true\false.
- assertNull\assertNotNulll: verifica que un objeto es nulo\no nulo.
- assertEquals\assertNotEquals: verifica que dos objetos sean iguales\distintos utilizando equals.
- assertSame\assertNotSame: verifica que dos objetos son el mismo\distintos con el operador «==».
- assertIterableEquals: verifica que dos objetos de tipo Iterable son iguales según el operador «==» y el equals de los objetos que contienen.
- assertLinesMatch: verifica que dos listas de String contienen las mismas cadenas.
Estos métodos presentan numerosas sobrecargas y algunas admiten una cadena con un mensaje, así como el uso de expresiones lambda. Si vemos su código, descubriremos que no hacen nada especial o sofisticado: llaman a fail cuando la condición no se verifica. Una convención curiosa (y práctica): su importación suele hacerse de manera estática.
Simplifiquemos nuestro ejemplo.
void testComparatorLessThanAssertJupiter() {
Comparator<Integer> integerComparator = Comparator.naturalOrder();
int result = integerComparator.compare(1, 2);
assertTrue(result < 0, "el resultado no es menor que cero");
}
El método assertAll permite agrupar varias aserciones escritas con lambdas.
@Test
@DisplayName("ejemplo de lambdas en assertAll")
void testOrderStringAssertJupiter() {
Comparator<String> stringComparator = Comparator.naturalOrder();
List<String> names = new ArrayList<>(3);
names.add("Daniel");
names.add("Juan");
names.add("Antonio");
names.sort(stringComparator);
assertAll("el orden de los nombres es incorrecto",
() -> assertEquals("Antonio", names.get(0)),
() -> assertEquals("Daniel", names.get(1)),
() -> assertEquals("Juan", names.get(2)));
}
AssertJ
A pesar de que las aserciones de Jupiter nos facilitan el trabajo, les encuentro dos defectos.
- En los assert que reciben dos parámetros, el primero es el esperado y el segundo el evaluado. Me resulta poco intuitivo, creo que sería más lógico que la signatura del método fuera la contraria.
- No son de mucha ayuda a la hora de escribir aserciones complejas que impliquen, por ejemplo, comprobar los valores de atributos concretos de los objetos contenidos en una lista.
AssertJ es una librería de aserciones independiente de cualquier marco de trabajo para pruebas. Con su API «fluida» se escriben verificaciones fáciles de leer siguiendo este patrón
comprobar que (A) ->verifica (condición 1) ->verifica (condición 2)
A esta estructura tan clara hay que añadir la disponibilidad de condiciones más numerosas y potentes que las proporcionadas por Jupiter.
Veamos un pequeño ejemplo. En primer lugar, añadimos la dependencia.
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
Reescribamos las aserciones.
@Test
@DisplayName("probando naturalOrder con enteros en JUnit 5 y AssertJ")
void testComparatorLessThanAssertJ() {
Comparator<Integer> integerComparator = Comparator.naturalOrder();
int result = integerComparator.compare(1, 2);
assertThat(result)
.isLessThan(0);
}
@Test
@DisplayName("ejemplo de colección con AssertJ")
void testOrderStringAssertJ() {
Comparator<String> stringComparator = Comparator.naturalOrder();
List<String> names = new ArrayList<>(3);
names.add("Daniel");
names.add("Juan");
names.add("Antonio");
names.sort(stringComparator);
assertThat(names)
.containsExactly("Antonio", "Daniel", "Juan");
}
Llamamos al método estático assertThat con el objeto a comprobar y vamos encadenando métodos para construir la aserción. Resulta claro y conciso.
Con withFailMessage se puede indicar el mensaje en caso de fallo, pero la descripción que AssertJ genera es bastante buena. En el siguiente ejemplo modifiqué testOrderStringAssertJ para provocar un incumplimiento en la aserción.
java.lang.AssertionError: Expecting: >["Antonio", "Daniel", "Juan"]< to contain exactly (and in same order): >["Antoanio", "Daniel", "Juan"] but some elements were not found: >["Antoanio"]< and others were not expected: >["Antonio"]<
La documentación es magnífica y está llena de ejemplos. Son muchos beneficios, así que vamos a aprovecharlos.
Excepciones
Echemos un vistazo rápido a unas aserciones peculiares: las que verifican excepciones. Quizás parezca extraño probar errores, pero dijimos en el capítulo anterior que hay que testear que la aplicación hace lo que tiene que hacer y que no hace lo que no debe.
El tratamiento de excepciones debería ser parte de un plan de pruebas que aspire a ser completo y relevante con el fin de asegurar el comportamiento adecuado del sistema ante cualquier eventualidad. En torno a este principio, se ha desarrollado una técnica de testing denominada Failure injection o inyección de errores considerada una buena práctica cuando se hacen pruebas de caja blanca.
Entre las aserciones de Jupiter encontramos varias con nombres tan descriptivos como assertThrows y assertDoesNotThrow. Veamos un ejemplo muy simple forzando una excepción.
@DisplayName("Ejemplos de aserciones para excepciones")
public class ExceptionAssertionTest {
@Test
@DisplayName("ejemplo de assert de excepción con Jupiter")
void testExceptionAssertJupiter() {
List strings = new ArrayList<>(0);
IndexOutOfBoundsException exception =
assertThrows(IndexOutOfBoundsException.class, () -> strings.get(0));
assertEquals("Index 0 out of bounds for length 0", exception.getMessage());
}
}
En esta prueba, la fase Act y la Assert son la misma porque el método a probar debe ser ejecutado por assertThrows dentro de una implementación de la interfaz funcional Executable. También se podría hacer esa implementación en la fase Arrange si consideramos que el código queda más claro. La excepción se ha recuperado en una variable para realizar comprobaciones adicionales y mejorar la rigurosidad del test.
Con AssertJ podemos hacer lo mismo pero de forma más elegante en un único bloque de código.
@Test
@DisplayName("ejemplo de assert de excepción con AssertJ")
void testExceptionAssertJ() {
List<String> strings = new ArrayList<>(0);
assertThatExceptionOfType(IndexOutOfBoundsException.class)
.isThrownBy(() -> strings.get(0))
.withMessage("Index 0 out of bounds for length 0");
}
Extensiones
Jupiter nos permite intervenir en el ciclo de ejecución de las pruebas y realizar las operaciones que queramos, tales como reutilizar código de configuración (Arrange) compartido por los tests o hacer un Arrange global de la clase. Estas acciones se implementan en métodos anotados del siguiente modo.
- @BeforeEach. Se ejecuta antes de cada método @Test. Debe ser estático.
- @AfterEach. Se ejecuta después de cada método @Test. Debe ser estático.
- @BeforeAll. Se ejecuta antes de empezar la ejecución del primer método @Test.
- @AfterAll. Se ejecuta después de que hayan finalizado todos los métodos @Test.

Comprobemos la secuencia de la figura.
package com.danielme.jakartaee.test;
import org.junit.jupiter.api.*;
import java.util.logging.Logger;
public class JupiterLifeCycleAnnotationsTest {
private final static Logger log = Logger.getLogger(JupiterLifeCycleAnnotationsTest.class.getName());
@BeforeAll
static void beforeAll() {
log.info("se ejecuta una única vez y antes de ejecutarse el primer test de la clase");
}
@AfterAll
static void afterAll() {
log.info("se ejecuta después de haberse ejecutado todos los tests");
}
@BeforeEach
void beforeEach() {
log.info(" se ejecuta antes de ejecutarse cada test");
}
@AfterEach
void afterEach() {
log.info(" se ejecuta despues de cada test");
}
@Test
void testFake1() {
log.info(" test");
}
@Test
void testFake2() {
log.info(" test 2");
}
}
La salida del log no deja lugar a dudas.
Feb 10, 2021 8:50:02 PM com.danielme.jakartaee.test.JupiterLifeCycleAnnotationsTest beforeEach INFO: se ejecuta antes de ejecutarse cada test Feb 10, 2021 8:50:02 PM com.danielme.jakartaee.test.JupiterLifeCycleAnnotationsTest testFake1 INFO: test Feb 10, 2021 8:50:02 PM com.danielme.jakartaee.test.JupiterLifeCycleAnnotationsTest afterEach INFO: se ejecuta despues de cada test Feb 10, 2021 8:50:02 PM com.danielme.jakartaee.test.JupiterLifeCycleAnnotationsTest beforeEach INFO: se ejecuta antes de ejecutarse cada test Feb 10, 2021 8:50:02 PM com.danielme.jakartaee.test.JupiterLifeCycleAnnotationsTest testFake2 INFO: test 2 Feb 10, 2021 8:50:02 PM com.danielme.jakartaee.test.JupiterLifeCycleAnnotationsTest afterEach INFO: se ejecuta despues de cada test Feb 10, 2021 8:50:02 PM com.danielme.jakartaee.test.JupiterLifeCycleAnnotationsTest afterAll INFO: se ejecuta después de haberse ejecutado todos los tests
Cuando empleamos las anotaciones anteriores estamos haciendo un uso indirecto y simplificado de Extension API. Una extensión consiste en un método que se ejecuta en cierto momento, llamado punto de extensión o extension point, del ciclo de ejecución del test. La aplicamos con la anotación @ExtendWith, válida tanto para un método como para una clase. Se pueden usar todas las extensiones que queramos en una prueba, superándose así la limitación de los runners de JUnit 4 que señalé al principio del capítulo.
Escribir una extensión es sencillo en lo que respecta a la creación de la estructura de su código, otro asunto bien diferente es su lógica. Basta con implementar las interfaces correspondientes a los puntos de extensión en los que queremos ejecutar nuestro código. Es lo que podemos hacer, por ejemplo, para replicar la funcionalidad de @BeforeAll: implementar la interfaz BeforeAllCallback.
No vamos a entrar en detalles. Sería raro que nos viésemos en la necesidad de escribir nuestras propias extensiones: numerosas librerías y marcos de trabajo ya las incluyen para integrarse con Jupiter. Esto lo veremos en el próximo capítulo en el que volveremos el mundo de Jakarta EE, así que dejo los ejemplos para ese momento.
Código de ejemplo
El código de ejemplo del capítulo se encuentra en GitHub (todos los proyectos están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.