Las pruebas automáticas suelen ser unas grandes olvidadas en el proceso de desarrollo de software. La primera víctima de las desviaciones de presupuestos y plazos de entrega. Pero te aseguro que es un crimen que no queda impune…
- ¿Por qué debería importarme el testeo automatizado?
- Presentando JUnit
- Tests con JUnit 4
- Comprobación de resultados (aserciones)
- AssertJ
- El patrón Arrange-Act-Assert
- Tests con parámetros
- Excepciones
- Timeout
- Configurar el orden de ejecución
- Suite de tests
- Cobertura de código
- Código de ejemplo
¿Por qué debería importarme el testeo automatizado?
Aunque la introducción al testing automatizado es la materia de este artículo, siempre es un buen momento para subrayar algunos de los beneficios de esta noble práctica:
- Lo más evidente es que facilita la detección de errores, aumentando la fiabilidad del software.
- Permite avanzar en la implementación de un proyecto asegurando, con un nivel de certeza no absoluto pero sí razonable, que los cambios y añadidos no rompen funcionalidades ya existentes y operativas. Utilizando un símil, supone programar con una red de seguridad. No te estrelles contra el piso; volver a ponerte en pie cuesta muchas horas de trabajo.
- Una vez implementadas, las pruebas automáticas se ejecutan todas las veces que sean necesarias sin ningún coste adicional en comparación con las realizadas de forma manual. Esto no implica que se descarten las pruebas manuales, sino que estas se inviertan donde aporten verdadero valor.
- Exige que el código de la aplicación sea fácil de testear, lo que redunda en un mejor diseño. Esto aumenta la calidad del código y facilita su mantenimiento. Un código difícil de probar es un buen indicador de un diseño deficiente.
- Las pruebas sirven de ejemplo y documentación del uso de las APIs y casos de uso del sistema.
Es tanto el valor que aportan las pruebas automáticas que son la base de una metodología denominada «Desarrollo dirigido por pruebas» (TDD). A grandes rasgos, basa el desarrollo de la aplicación en las pruebas automáticas: primero se diseñan los tests y luego se escribe el código que los verifica.
La clasificación de los tipos de test o pruebas que pueden realizarse es muy extensa (y opinable). El nivel más básico lo marcan las pruebas unitarias. Con ellas probamos a fondo unidades mínimas de código de manera aislada del resto del código o de los servicios de un framework. Debido a esto último, en muchos casos simularemos la interacción del código que probamos con otros componentes mediante dobles de test. El objetivo final es contar con una multitud de pruebas sencillas de escribir y de ejecución muy rápida, en el orden de unos pocos milisegundos, que iremos lanzando con gran frecuencia mientras trabajamos.
Presentando JUnit
JUnit es el marco de trabajo o framework de testing para Java más popular. Aunque su nombre sugiere que se centra en las pruebas unitarias, en realidad permite implementar cualquier tipo de prueba automatizada. Esto se debe a que su principal objetivo es ofrecer una infraestructura sencilla para la escritura de pruebas en general.
Gracias a los mecanismos de personalización de JUnit, las pruebas que hagamos con él se integran a las mil maravillas con librerías como Mockito (dobles de test), Selenium (interacción con navegadores) o Testcontainers (gestión de contenedores Docker). ¿Usas Spring \ Spring Boot? También se fusiona con JUnit, lo que nos permitirá inyectar beans en las pruebas.
Quizás hayas observado que cuando se habla de JUnit se suele indicar su versión. No es por capricho; existe una razón poderosa para ello. La versión 5 (septiembre de 2017) fue toda una revolución porque se creó desde cero con el fin de ofrecer una plataforma potente y extensible sobre la que construir librerías de testing. Incluye una bautizada como Jupiter; las pruebas desarrolladas con ella se suelen denominar «pruebas de JUnit 5. Por eso es importante distinguir entre las versiones 4 y 5.
Pese a las ventajas de la versión 5, JUnit 4 sigue vivo y coleando debido al mantenimiento de proyectos antiguos y a Android. No olvidemos que fue un estándar durante más de diez años. Y, al final del día, hace el trabajo.
La buena noticia es que no se requiere tocar el código de esos proyectos para dar el salto a la nueva versión. JUnit 5 provee la librería de testing Vintage para ejecutar tests de JUnit 4 sobre JUnit 5. Así, no tendrás que cambiar nada en las pruebas que ya tuvieras, y podrás hacer las nuevas con Jupiter.
En este tutorial, solo trataré el viejo (y bueno) JUnit 4. Pero te animo a usar Jupiter siempre que puedas. Tienes una introducción en el siguiente tutorial.
Tests con JUnit 4
El pom
En este tutorial vamos a usar un proyecto Maven que encontrarás en GitHub. De inicio (luego lo modificaremos) el pom solo tiene las dependencias de la librería de logging Apache Log4j 2 y de JUnit 4. El ámbito (scope) es test porque no queremos que se incluya en los artefactos finales del proyecto (jar, .war, .ear).
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
Si quieres usar JUnit 4 en JUnit 5, porque también vas a escribir pruebas con Jupiter, es suficiente con incorporar la librería Vintage. No necesitas la dependencia de JUnit 4.
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
Nada de lo que veamos cambia por el hecho de tener Vintage. Eso sí, ten cuidado porque existen varias anotaciones, como @Test, de igual nombre en Vintage y Jupiter. Elige la adecuada en cada momento.
Las anotaciones básicas
Los tests de JUnit 4 se implementan en clases POJO sin ninguna particularidad; JUnit conoce los tests a ejecutar en función de las anotaciones que encuentre. Estas clases pueden tener cualquier nombre, aunque es habitual seguir la convención de utilizar el sufijo «Test». En el caso de Maven, se ubican en la carpeta /src/test/java. Los recursos que sean específicos de las pruebas estarán en el directorio /src/test/resources.
Al ejecutarse una clase con JUnit se invocarán los métodos que contengan las siguientes anotaciones. Deben ser públicos, sin retorno (void) y sin argumentos (más adelante veremos una excepción).
- @Test – cada método que implemente un test. Lo veremos más adelante. Nota: los test (métodos y clases) marcados con @Ignore no se ejecutarán.
- @Before – se invoca antes de la ejecución de cada @Test.
- @After – se invoca después de la ejecución de cada @Test.
- @BeforeClass – método estático. Se invoca una única vez al ejecutarse la clase antes de cualquier método @Test o @Before. Por ejemplo, podríamos utilizar este método para realizar una única vez configuraciones requeridas por todas las pruebas.
- @AfterClass – método estático. Equivalente al anterior, pero ahora el método es el último que se invoca al ejecutarse la clase.
Para los métodos anotados con @Before y @BeforeClass, la convención habitual consiste en nombrarlos con el prefijo «setUp». En aquellos anotados con @After y @AfterClass se recurre al prefijo «tearDown».
En los métodos @Test se suele emplear «test» como prefijo o sufijo. Lo importante es elegir nombres descriptivos para aumentar la legibilidad del código y, sobre todo, de los resultados de las pruebas. Recomendación válida para cualquier elemento del código (métodos, clases, variables).
Algunos programadores -no soy uno de ellos- apuestan por el uso de nombres largos con la notación Pascal-case, esto es, utilizando minúsculas y el carácter «_» como separador de palabras.
Con la siguiente clase, que no prueba nada, veremos en detalle el flujo de ejecución.
package com.danielme.blog.testing;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.*;
public class LifecycleTest {
private static final Logger logger = LogManager.getLogger(LifecycleTest.class);
@BeforeClass
public static void setUpClass() {
logger.info("");
}
@AfterClass
public static void tearDownClass() {
logger.info("");
}
@Before
public void setUpTest() {
logger.info("");
}
@After
public void tearDownTest() {
logger.info("");
}
@Test
public void testNothing1() {
logger.info("");
}
@Test
public void testNothing2() {
logger.info("");
}
}
Ejecutando las pruebas
Podemos ejecutar todas las pruebas del proyecto con mvn test, o una clase en concreto con mvn test -Dtest=clase. Asimismo, al instalarse el artefacto (el .jar. el .war…) del proyecto con la orden install de Maven, se ejecutarán las pruebas, de tal modo que si hay algún error el artefacto no se generará. En este último escenario, podemos desactivar la ejecución de las pruebas con la opción -DskipTests.
Esta magia es cosa del plugin Maven Surefire. No lo verás en el pom del proyecto de ejemplo, pues ya está implícito en todo proyecto Maven. Solo hará falta declararlo cuando se necesite indicar su versión. Es el caso de las pruebas de Jupiter: se precisa la versión 2.22 como mínimo.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
</plugin>
</plugins>
</build>
Sea como fuere, esta es la salida de la ejecución de LifecycleTest.
Running com.danielme.blog.testing.LifecycleTest 2021-04-16 17:20:32 INFO LifecycleTest:16.setUpClass - 2021-04-16 17:20:32 INFO LifecycleTest:26.setUpTest - 2021-04-16 17:20:32 INFO LifecycleTest:36.testNothing1 - 2021-04-16 17:20:32 INFO LifecycleTest:31.tearDownTest - 2021-04-16 17:20:32 INFO LifecycleTest:26.setUpTest - 2021-04-16 17:20:32 INFO LifecycleTest:41.testNothing2 - 2021-04-16 17:20:32 INFO LifecycleTest:31.tearDownTest - 2021-04-16 17:20:32 INFO LifecycleTest:21.tearDownClass - Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec

En la carpeta /target/surefire-reports/ encontraremos los informes, en formato txt y xml, con los resultados de la ejecución. La siguiente captura se corresponde con el proyecto final de todo el tutorial.

Otra manera de ejecutar las pruebas es recurrir al soporte proporcionado por el entorno de desarrollo (IDE) que utilicemos.
Eclipse e IntelliJ ofrecen de serie integración con JUnit. Para lanzar los tests, primero pulsamos con el botón derecho en una clase con pruebas, en varias de ellas o en un paquete con clases de este tipo. A continuación, en el menú contextual que se despliega, seleccionamos Run. Es lo que muestra la siguiente imagen (IntelliJ a la izquierda y Eclipse a la derecha).
Los resultados se ofrecen de forma visual y clara.
Comprobación de resultados (aserciones)
Un test debe considerarse exitoso (color verde en el IDE) si el código bajo prueba produce el resultado esperado. Si no, se considera fallido (amarillo), circunstancia que indicamos invocando a la función fail. Y cuando no se pueda completar la ejecución del test (por ejemplo, debido a una excepción inesperada), entonces se considera erróneo (color rojo).
Los dos tests anteriores son verdes porque no llamaron a fail. De hecho, no hacen nada. Veamos un ejemplo más realista basado en el siguiente enumerado.
package com.danielme.blog.testing;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public enum UserService {
INSTANCE;
private final List<String> users = Arrays.asList("Bombur", "Bofur", "Bifur", "Nori", "Ori", "Dori", "Thorin",
"Balin", "Dwalin", "Oin", "Gloin", "Fili", "Kili");
public List<String> getUsersByName(String name) {
if (name == null) {
return Collections.emptyList();
}
return users.stream()
.filter(u -> u.toUpperCase().contains(name.toUpperCase()))
.collect(Collectors.toList());
}
public String getUserByPosition(int pos) {
return users.get(pos);
}
}
UserService encapsula una lista de nombres de usuario muy peculiar: los enanos de la novela «El hobbit». Ofrece un método que devuelve una lista con los nombres que contienen la cadena pasada como argumento, ignorando la capitalización.
Hagamos una búsqueda por «oin», y comprobemos que se devuelven los dos nombres esperados: «Gloin» y «Oin». Cuando esto no ocurra, hay que invocar a fail con un mensaje que explique la razón del fallo.
@Test
public void testGetUsersByNameWithFail() {
List<String> usersByName = UserService.INSTANCE.getUsersByName("oin");
if (usersByName.size() != 2) {
fail("size must be 2");
} else if (!usersByName.contains("Gloin")) {
fail("Gloin not found!!");
} else if (!usersByName.contains("Oin")) {
fail("Oin not found!!");
}
}
testGetUsersByNameWithFail debería ser correcto. Si quieres verlo fallar, cambia el nombre de la línea tres.
testGetUsersByName(com.danielme.blog.testing.UserServiceTest) Time elapsed: 0.005 sec <<< FAILURE! java.lang.AssertionError: size must be 2 at org.junit.Assert.fail(Assert.java:88) at com.danielme.blog.testing.UserServiceTest.testGetUsersByName(UserServiceTest.java:53) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27) at org.junit.rules.ExpectedException$ExpectedExceptionStatement.evaluate(ExpectedException.java:239) at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:298) at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:292) at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.lang.Thread.run(Thread.java:745) Results : Failed tests: testGetUsersByName(com.danielme.blog.testing.UserServiceTest): size must be 2
Las comprobaciones de los resultados devueltos por el código bajo prueba se denominan aserciones. Implementarlas de la forma que lo hemos hecho es un fastidio.
Para facilitarnos la vida, JUnit provee una colección de métodos estáticos en la clase Assert. Realizan validaciones sobre sus argumentos, de tal modo que invocarán a fail cuando no verifiquen la condición esperada.
Mira ahora.
@Test
public void testGetUsersByNameWithAssertion() {
List<String> usersByName = UserService.INSTANCE.getUsersByName("oin");
assertEquals(2, usersByName.size());
assertTrue("Gloin not found!!", usersByName.contains("Gloin"));
assertTrue("Oin not found!!", usersByName.contains("Oin"));
}
Se entiende mejor.
Nota. En los tests sigo una convención que consiste en importar de forma estática los métodos estáticos pertenecientes a librerías de testing.
Es una buena idea utilizar las sobrecargas de los métodos assert que admiten una cadena con el mensaje para fail. En el caso concreto de assertEquals no lo hice porque la descripción que genera es bastante descriptiva.
Failed tests: testGetUsersByName(com.danielme.blog.testing.UserServiceTest): expected: 2 but was: 3
AssertJ
Un detalle interesante del último ejemplo es que en los assert que reciben dos parámetros, como assertEquals, el primero representa el valor esperado y el segundo es la respuesta que estamos revisando. Este criterio, al menos a mí, me parece confuso. Creo que lo contrario sería más natural.
No obstante, el verdadero problema de las aserciones de JUnit 4 es que resultan rudimentarias y básicas. Por ello, siempre recomiendo AssertJ, una sensacional librería de aserciones. Con su API fluida escribimos 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 ofrecidas por JUnit.
Agrego AssertJ al pom.
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
Usémosla para reescribir testGetUsersByName.
@Test
public void testGetUsersByNameWithAssertJ() {
List<String> usersByName = UserService.INSTANCE.getUsersByName("oin");
assertThat(usersByName)
.hasSize(2)
.containsExactlyInAnyOrder("Gloin", "Oin");
}
El anterior fragmento de código demuestra el uso típico de la API de AssertJ. Vemos lo fácil e intuitivo que resulta aplicar aserciones rigurosas sobre una lista, casuística con la que me encuentro a menudo. La estructura que indiqué líneas atrás se construye imponiendo condiciones al objeto que Assertions#assertThat devuelve.
comprobar que (usersByName)
->tiene tamaño 2
->contiene exactamente en cualquier orden "Gloin" y "Oin"
Si prestas atención al código, deducirás que la llamada a hasSize es redundante: la existencia exacta de dos elementos ya se comprueba en containsExactlyInAnyOrder. Por ello, la he eliminado del proyecto
La documentación de AssertJ está llena de ejemplos prácticos. Te animo a revisarla para que descubras lo que esta librería puede hacer por ti.
El patrón Arrange-Act-Assert
Los saltos de línea que he puesto en los tests no son casuales. Una buena manera de estructurar el código de una prueba consiste en seguir el patrón «Arrange-Act-Assert» (AAA), simple pero efectivo.
- Arrange (Organizar). Se configuran los elementos necesarios para realizar la prueba. Esta tarea puede incluir instanciar las clases a probar y preparar los objetos dobles de tests.
- Act (Actuar). Se ejecuta la operación a validar.
- Assert (Afirmar). Comprobamos que el resultado de «Act» es el esperado.
A veces, la fase Arrange resulta innecesaria. Es posible que su lógica ya se realice en un método de tipo @Before o @BeforeAll. O, simplemente, no hay que hacer nada.
Si la prueba que estamos escribiendo efectúa más de una acció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.
Tests con parámetros
Al hacer pruebas exhaustivas, a menudo necesitamos repetir el mismo test con diferentes valores de entrada para el método que estamos probando. ¿Cómo lo hacemos? Resulta tentador escribir un test que contenga un bucle o, peor todavía, copiar y pegar el test…
¡No lo hagas! Los tests parametrizados de JUnit 4 resuelven este escenario. Pero su uso es algo tosco, así que prefiero utilizar la librería JUnitParams.
La incluimos en nuestro pom.
<dependency>
<groupId>pl.pragmatists</groupId>
<artifactId>JUnitParams</artifactId>
<version>${junitparams.version}</version>
<scope>test</scope>
</dependency>
Vamos a crear un test para getUsersByName que se ejecutará tres veces. Cada vez usaremos una cadena de búsqueda distinta que implica un resultado diferente (el número de elementos de la lista).
name | result |
oin | 2 |
balin | 1 |
gimli | 0 |
@RunWith(JUnitParamsRunner.class)
public class UserServiceTest {
@Test
@Parameters({"oin, 2", "balin, 1", "gimli, 0"})
public void testGetUsersByNameParameterised(String name, int result) {
List<String> usersByName = UserService.INSTANCE.getUsersByName(name);
assertThat(usersByName)
.hasSize(result);
}
Indicamos a JUnit que ejecute las pruebas con el runner (*) JUnitParamsRunner. Cada par «nombre \ resultado» se define en el método con el test con la anotación @Parameters. Los recibimos como argumentos.
(*) Un runner es una especialización de Runner y es responsable de ejecutar las pruebas. El sistema de runners se suele usar para integrar JUnit 4 con librerías y marcos de trabajo (Mockito, Spring, Arquillian…). Pero tiene una limitación importante: solo puede aplicarse un runner a una clase de prueba. El mecanismo equivalente en JUnit 5 (extensiones) no presenta este inconveniente.

La captura anterior muestra el resultado de la ejecución del test parametrizado. Fíjate que en realidad son tres pruebas distintas, una por cada juego de parámetros. Aunque falle alguna, siempre se ejecutan todas. En consecuencia, es mejor crear un test parametrizado que uno «normal» con un bucle.
En la documentación encontrarás otros usos de @Parameters.
Excepciones
En ocasiones, el resultado que queremos validar es el lanzamiento de una excepción. Podemos simular este escenario llamando al método UserService#getUserByPosition con una posición que no exista. Esto provoca que la lista de usuarios lance IndexOutOfBoundsException. ¿Cómo chequeamos este comportamiento?
JUnit propone cuatro formas:
- A mano, con try-catch y fail.
@Test
public void testGetUserByPositionExceptionFail() {
try {
UserService.INSTANCE.getUserByPosition(-1);
fail("Exception not thrown");
} catch (IndexOutOfBoundsException ex) {
assertThat(ex.getMessage())
.contains("-1");
}
}
- Indicando la excepción que debe producirse en la anotación @Test.
@Test(expected = IndexOutOfBoundsException.class)
public void testGetUserByPositionExceptionAnnotation() {
UserService.INSTANCE.getUserByPosition(-1);
}
- Con la regla ExpectedException, incluida en JUnit, del siguiente modo:
1-.Declarar la regla en la clase de prueba.
@Rule
public ExpectedException exception = ExpectedException.none();
2-. Indicar al inicio del test la excepción esperada. Es posible validar su mensaje.
@Test
public void testGetUserByPositionExceptionRule() {
exception.expect(IndexOutOfBoundsException.class);
exception.expectMessage("-1");
UserService.INSTANCE.getUserByPosition(-1);
}
- Desde JUnit 4.13 (enero de 2020), contamos con los métodos assertThrows.
@Test
public void testExceptionAssertJUnit413() {
assertThrows(IndexOutOfBoundsException.class, () -> UserService.INSTANCE.getUserByPosition(-1));
}
Las tres primeras opciones no me convencen. La número uno origina un código difícil de leer. En las otras dos no se puede precisar la instrucción que debe lanzar la excepción. Además, la regla se marcó obsoleta (@Deprecated). La cuarta es, por tanto, la alternativa que recomiendo.
Con todo, la solución que más me agrada consiste en emplear AssertJ porque permite validar a fondo el lanzamiento de excepciones.
@Test
public void testExceptionAssertJ() {
assertThatThrownBy(() -> UserService.INSTANCE.getUserByPosition(-1))
.isInstanceOf(IndexOutOfBoundsException.class)
.hasMessageContaining("-1");
}
testExceptionAssertJ comprueba que el código ejecutado en una expresión lambda lanza una excepción de cierto tipo que contenga cierta cadena en su mensaje.
Timeout
Es posible establecer en cada prueba un timeout o tiempo máximo de ejecución en milisegundos. Si pasado ese periodo el test no ha finalizado, su resultado será fallido.
El timeout se configura de forma similar a la validación de excepciones:
- Con la anotación @Test : @Test(timeout=5000)
- Con una regla que se aplicará a cada uno de los test de la clase.
@Rule
public Timeout timeout = Timeout.millis(5000);
Configurar el orden de ejecución
¿Cómo se puede establecer el orden de ejecución de los tests? Esta es una de las preguntas más frecuentes acerca de JUnit.
El planteamiento es erróneo: un test ha de ser autónomo y nunca depender de otro. Por consiguiente, no debería ser necesario ejecutarlos siempre en el mismo orden.
En cualquier caso, existe una respuesta: marcando una clase de prueba con @FixMethodOrder(MethodSOrter.NAME_ASCENDING) se fuerza la ejecución de sus tests en orden alfabético.
Suite de tests
Una suite de JUnit consiste en una agrupación de varias clases con tests que se ejecutarán de manera conjunta. Por ejemplo, podemos crear una suite con los tests que comprueban la capa de negocio, otra con las pruebas de integración, etcétera.
Definir una suite es trivial: basta con crear una clase y usar un par de anotaciones. @RunWith debe solicitar la ejecución de la clase con el runner para suites. Con @Suite.SuiteClasses especificamos las clases que componen la suite.
package com.danielme.blog.testing;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
UserServiceTest.class,
LifecycleTest.class
})
public class TestingSuite {
//clase vacia
}
TestingSuite se lanza como cualquier clase de prueba. Ejecuta las clases UserServiceTest y LifecycleTest respetando el orden indicado en la anotación.
Cobertura de código
La cobertura (code coverage) es una métrica que informa del código que fue ejecutado durante la realización de las pruebas. Si bien es bastante útil, no debemos considerarla el valor absoluto que indica la calidad de los tests. Hay que sopesar otros factores, tales como la rigurosidad de las aserciones o la similitud entre las configuraciones de test y de producción en el caso de pruebas de integración.
Debemos poner el foco en la utilidad de las pruebas y no escribirlas con el único propósito de conseguir altas coberturas.
Eclipse incluye una herramienta que mide la cobertura de los tests ejecutados desde el IDE con la opción Run -> Coverage.

Se indica el código ejecutado (verde) y el que no (rojo), así como las bifurcaciones o ramas en las que no se ha entrado por todas las condiciones (amarillo).

En la vista Coverage se muestran las estadísticas.

Por supuesto, IntelliJ no es menos y cuenta con la misma funcionalidad.

Código de ejemplo
El proyecto está en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.
Otros tutoriales sobre testing
Introducción al testing automático
Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos embebidas.