Introducción
Contenido
JUnit es un marco de prueba popular para Java. El uso simple es muy sencillo y JUnit 5 trajo algunas diferencias y comodidades en comparación con JUnit 4.
El código de prueba está separado del código del programa real y, en la mayoría de los IDE, los resultados / salida de la prueba también están separados de la salida del programa, lo que proporciona una estructura legible y conveniente.
Instalación de JUnit 5
Instalar JUnit es tan simple como incluir las dependencias:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.0-RC1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.0-RC1</version>
<scope>test</scope>
</dependency>
Puede elegir simplemente crear las clases de prueba en la misma carpeta que el resto de su código, pero se recomienda tener un directorio separado para las pruebas. Otra cosa a tener en cuenta son las convenciones de nomenclatura. Si deseamos probar completamente nuestro código, cada clase debe tener una clase de prueba correspondiente llamada – [classname]Test
.
Generalmente, una estructura de proyecto recomendada es:
Nota: Es muy recomendable que importe JUnit5 usando el static
modificador, hará que el uso de los métodos proporcionados sea mucho más limpio y más legible.
Diferencias entre JUnit 4 y JUnit 5
Una de las ideas principales detrás de la nueva versión de JUnit es utilizar las características que Java 8 trajo a la mesa (principalmente lambdas) para facilitar la vida de todos. Se han cambiado algunas cosas menores: el mensaje opcional de que se imprimiría una aserción si fallaba es ahora el último argumento «opcional», en lugar de ser inconvenientemente el primero.
JUnit 5 consta de tres proyectos (JUnit Platform, JUnit Jupiter y JUnit Vintage), por lo que habrá varias importaciones diferentes, aunque JUnit Jupiter será nuestro enfoque principal.
Algunas otras diferencias incluyen:
- El JDK mínimo para JUnit 4 era JDK 5, mientras que JUnit 5 requiere al menos JDK 8
- El
@Before
,@BeforeClass
,@After
, y@AfterClass
las anotaciones son ahora el más fácil de leer como los@BeforeEach
,@BeforeAll
,@AfterEach
, y@AfterAll
anotaciones @Ignore
es ahora@Disable
@Category
es ahora@Tag
- Soporte para clases de prueba anidadas y una fábrica de pruebas adicional para pruebas dinámicas
La anotación @Test
Usaremos una clase de calculadora simple para demostrar las capacidades básicas de JUnit. Por ahora, nuestra Calculator
clase se ve así:
public class Calculator {
float add(float a, float b) {
return a + b;
}
int divide(int a, int b) {
return a/b;
}
}
No hace nada especial, pero nos permitirá realizar las pruebas. Según las convenciones de nomenclatura, CalculatorTest
nace la clase:
class CalculatorTest {
@Test
void additionTest() {
Calculator calc = new Calculator();
assertEquals(2, calc.add(1,1), "The output should be the sum of the two arguments");
}
}
La @Test
anotación le dice a la JVM que el siguiente método es una prueba. Esta anotación es necesaria antes de cada método de prueba.
El método assertEquals()
y todos los métodos de «afirmación» funcionan de manera similar: afirman (es decir, se aseguran) de que lo que estamos comprobando es true
. En este caso, estamos afirmando que los dos argumentos que pasamos son iguales (consulte la Nota a continuación), en caso de que no lo sean, la prueba fallará.
El primer argumento es generalmente el valor de retorno esperado y el segundo es el valor de retorno real del método que estamos probando. Si estos dos son iguales, la afirmación está satisfecha y la prueba pasa.
El tercer argumento es opcional pero muy recomendable: es el mensaje personalizado que aparecerá cuando una prueba no salga como debería. Puede que no importe con los programas pequeños, pero es una buena práctica agregar estos mensajes para que quien trabaje con su código más adelante (o un futuro usted) pueda descubrir fácilmente lo que no funcionó.
Ejecutamos las pruebas simplemente ejecutando la CalculatorTest
clase (podemos hacerlo aunque no tenga un main
método):
Si cambiamos la assertEquals()
línea a algo que no es correcto, como:
assertEquals(1, calc.add(1,1), "The output should be the sum of the two arguments");
Recibiremos un mensaje de falla de prueba adecuado:
Nota: Es muy importante comprender que en assertEquals()
realidad usa el .equals()
método y no el ==
operador. Hay un método JUnit separado llamado assertSame()
que usa en ==
lugar de .equals()
.
Métodos de afirmación
JUnit 5 viene con muchos métodos de afirmación . Algunos de ellos son solo métodos de conveniencia que se pueden reemplazar fácilmente por un método assertEquals()
o assertSame()
. Sin embargo, se recomienda utilizar estos métodos de conveniencia en su lugar, para facilitar la lectura y el mantenimiento.
Por ejemplo, la llamada assertNull(object, message)
se puede reemplazar por assertSame(null, object, message)
, pero se recomienda la forma anterior.
Echemos un vistazo a las afirmaciones a nuestra disposición. En general, se explican por sí mismos:
assertEquals()
yassertNotEquals()
assertSame()
yassertNotSame()
assertFalse()
yassertTrue()
assertThrows()
afirma que el método generará una excepción dada, cuando se enfrente al valor de retorno del método probadoassertArrayEquals(expectedArray, actualArray, optionalMsg)
compara las dos matrices y pasa solo si tienen los mismos elementos en las mismas posiciones; de lo contrario, falla. Si ambas matrices lo sonnull
, se consideran iguales.assertIterableEquals(Iterable<?> expected, Iterable<?> actual, optionalMsg)
se asegura de que los iterables esperados y reales sean profundamente iguales. Dado que este método toma anIterable
como los dos argumentos, los iterables que pasamos no necesitan ser del mismo tipo (podemos pasar ayLinkedList
anArrayList
, por ejemplo). Sin embargo, sus iteradores deben devolver elementos iguales en el mismo orden que los demás. Nuevamente, si ambos lo sonnull
, se consideran iguales.assertLinesMatch(List<String> expected, List<String> actual, optionalMsg)
es un método un poco más complejo, ya que toma varios pasos antes de declarar que los argumentos pasados no son iguales y funciona solo conString
s:- Verifica si
expected.equals(actual)
regresatrue
, si lo hace, pasa a las siguientes entradas. - Si el Paso 1 no regresa
true
, laexpected
cadena actual se trata como una expresión regular, por lo que el método verificaactual.matches(expected)
si lo hace y, si lo hace, pasa a las siguientes entradas. - Si ninguno de los dos pasos anteriores regresa
true
, el último intento que hace el método es verificar si la siguiente línea es una línea de avance rápido. Una línea de avance rápido comienza y termina con «>>», entre los cuales hay un número entero (omite el número de líneas designadas) o una cadena. <T extends Throwable> T assertThrows(Class<T> expectedType, Executable exec, optionalMsg)
comprueba que la ejecución deExecutable
arroja una excepción deexpectedType
y devuelve esa excepción. Si no se lanza ninguna excepción o si la excepción lanzada no es delexpectedType
, la prueba falla.assertTimeout(Duration timeout, Executable exec, optionalMsg)
comprueba queexec
completa su ejecución antes de que se exceda el tiempo de espera dado. Dado queexec
se ejecuta en el mismo hilo que el del código de llamada, la ejecución no se interrumpirá de forma preventiva si se excede el tiempo de espera. En otras palabras,exec
finaliza su ejecución independientemente detimeout
, el método simplemente verifica después si se ejecutó lo suficientemente rápido.assertTimeoutPreemptively(Duration timeout, Executable exec, optionalMsg)
comprueba que la ejecución deexec
completa antes de que se exceda el tiempo de espera dado, pero a diferencia delassertTimeout
método, este método se ejecutaexec
en un subproceso diferente y abortará preventivamente la ejecución sitimeout
se excede el proporcionado .assertAll(Exectutable... executables) throws MultipleFailuresError
yassertAll(Stream<Executable> executables) throws MultipleFailuresError
hace algo muy útil. Es decir, si quisiéramos usar varias afirmaciones en una prueba (no es necesariamente malo si lo hacemos), sucedería algo muy molesto si todas salieran mal. A saber:@Test void additionTest() { Calculator calc = new Calculator(); assertEquals(100, calc.add(1,1), "Doesn't add two positive numbers properly"); assertEquals(100, calc.add(-1,1), "Doesn't add a negative and a positive number properly"); assertNotNull(calc, "The calc variable should be initialized"); }
Cuando falla la primera afirmación, no veremos cómo fueron las otras dos. Lo cual puede ser especialmente frustrante, ya que puede corregir la primera afirmación con la esperanza de que solucione toda la prueba, solo para descubrir que la segunda afirmación también falló, solo que no la vio ya que la primera afirmación que falló «ocultó» ese hecho. :
assertAll()
resuelve este problema ejecutando todas las afirmaciones y luego mostrándole el error incluso si fallaron varias afirmaciones. La versión reescrita sería:@Test void additionTest() { Calculator calc = new Calculator(); assertAll( () -> assertEquals(100, calc.add(1,1), "Doesn't add two positive numbers properly"), () -> assertEquals(100, calc.add(-1,1), "Doesn't add a negative and a positive number properly"), () -> assertNotNull(calc, "The calc variable should be initialized") ); }
Ahora obtendremos un resultado de prueba más informativo:
Es bueno entender que
assertAll()
básicamente verifica si alguno de los ejecutables arroja una excepción, ejecutándolos todos independientemente, y todos los que arrojan una excepción se agregan en elMultipleFailuresError
que arroja el método. Sin embargo, para problemas serios, comoOutOfMemoryError
la ejecución se detendrá inmediatamente y la excepción se volverá a lanzar tal cual, pero enmascarada como una excepción no verificada (tiempo de ejecución).
Nota: Es posible que haya notado que String optionalMsg
se excluye de las declaraciones del método. JUnit 5 proporciona una pequeña optimización al optionalMsg
. Por supuesto, podemos usar un simple String
como nuestro optionalMsg
; sin embargo, independientemente de cómo vaya la prueba (si falla o no), Java seguirá generando eso String
, aunque es posible que nunca se imprima. Esto no importa cuando hacemos algo como:
assertEquals(expected, actual, "The test failed for some reason");
Pero si tuviéramos algo parecido a:
assertEquals(expected, actual, "The test failed because " + (Math.sqrt(50) + Math.scalb(15,7) + Math.cosh(10) + Math.log1p(23)) + " is not a pretty number");
Realmente no desea que se optionalMsg
cargue algo así, independientemente de si Java planea imprimirlo.
La solución es usar un Supplier<String>
. De esta manera podemos utilizar los beneficios de la evaluación perezosa, si nunca ha oído hablar del concepto, es básicamente Java diciendo «No calcularé nada que no necesite. ¿Necesito esto String
ahora? ¿No? Entonces no lo creará «. La evaluación perezosa aparece varias veces en Java.
Esto se puede hacer simplemente agregando () ->
antes de nuestro mensaje opcional. Para que se convierta en:
assertEquals(expected, actual, () -> "The test failed because " + (Math.sqrt(50) + Math.scalb(15,7) + Math.cosh(10) + Math.log1p(23)) + " is not a pretty number");
Esta es una de las cosas que no eran posibles antes de JUnit 5, porque Lambdas no se introdujo en Java en ese momento y JUnit no pudo aprovechar su utilidad.
Prueba de anotaciones
En esta parte, presentaremos algunas otras anotaciones, además de la @Test
anotación necesaria . Una cosa que debemos entender es que para cada método de prueba, Java crea una nueva instancia de la clase de prueba.
Es una mala idea declarar variables globales que se cambian dentro de diferentes métodos de prueba, y es una idea especialmente mala esperar cualquier tipo de orden de prueba, ¡no hay garantías de en qué orden se ejecutarán los métodos de prueba!
Otra mala idea es tener que inicializar constantemente la clase que queremos probar si no es necesario. Veremos cómo evitarlo pronto, pero antes de eso, echemos un vistazo a las anotaciones disponibles:
@BeforeEach
: Se llama a un método con esta anotación antes de cada método de prueba, muy útil cuando queremos que los métodos de prueba tengan algún código en común. Los métodos deben tener unvoid
tipo de retorno, no deben serprivate
y no deben serstatic
.@BeforeAll
: Un método con esta anotación se llama solo una vez, antes de que se ejecute cualquiera de las pruebas, se usa principalmente en lugar de@BeforeEach
cuando el código común es costoso, como establecer una conexión de base de datos. ¡El@BeforeAll
método debe ser elstatic
predeterminado! Tampoco debe serprivate
y debe tener unvoid
tipo de retorno.@AfterAll
: Un método con esta anotación se llama solo una vez, después de que se haya llamado a cada método de prueba. Normalmente se utiliza para cerrar conexiones establecidas por@BeforeAll
. El método debe tener unvoid
tipo de retorno, no debe serprivate
y debe serstatic
.@AfterEach
: Se llama a un método con esta anotación después de que cada método de prueba finaliza su ejecución. Los métodos deben tener unvoid
tipo de retorno, no deben serprivate
y no deben serstatic
.
Para ilustrar cuándo se ejecuta cada uno de estos métodos, agregaremos algo de sabor a nuestra CalculatorTest
clase y, mientras lo hacemos, demostraremos el uso del assertThrows()
método:
class CalculatorTest {
Calculator calc;
@BeforeAll
static void start() {
System.out.println("inside @BeforeAll");
}
@BeforeEach
void init() {
System.out.println("inside @BeforeEach");
calc = new Calculator();
}
@Test
void additionTest() {
System.out.println("inside additionTest");
assertAll(
() -> assertEquals(2, calc.add(1,1), "Doesn't add two positive numbers properly"),
() -> assertEquals(0, calc.add(-1,1), "Doesn't add a negative and a positive number properly"),
() -> assertNotNull(calc, "The calc variable should be initialized")
);
}
@Test
void divisionTest() {
System.out.println("inside divisionTest");
assertThrows(ArithmeticException.class, () -> calc.divide(2,0));
}
@AfterEach
void afterEach() {
System.out.println("inside @AfterEach");
}
@AfterAll
static void close() {
System.out.println("inside @AfterAll");
}
}
Lo que nos da el resultado de:
inside @BeforeAll
inside @BeforeEach
inside divisionTest
inside @AfterEach
inside @BeforeEach
inside additionTest
inside @AfterEach
inside @AfterAll
Esto también nos muestra que, a pesar de que el additionTest()
método se declara primero, no garantiza que se ejecutará primero.
Otras anotaciones
Antes de JUnit 5, los métodos de prueba no podían tener ningún parámetro, pero ahora sí. Los usaremos mientras demostramos las nuevas anotaciones.
@Discapacitado
Una anotación simple y útil que simplemente deshabilita cualquier método de prueba, es decir, la prueba no se ejecutará y el resultado de la prueba mostrará que la prueba en particular fue deshabilitada:
@Disabled
@Test
void additionTest() {
// ...
}
Da el siguiente resultado para ese método de prueba:
Te puede interesar:Patrones de diseño de comportamiento en Javavoid main.CalculatorTest.additionTest() is @Disabled
@DisplayName
Otra anotación simple que cambia el nombre mostrado del método de prueba.
@DisplayName("Testing addition")
@Test
void additionTest() {
// ...
}
@Etiqueta
La @Tag
anotación es útil cuando queremos crear un «paquete de prueba» con las pruebas seleccionadas. Las etiquetas se utilizan para filtrar qué pruebas se ejecutan:
class SomeTest {
@Tag("a")
@Test
void test1() {
// ...
}
@Tag("a")
@Test
void test2() {
// ...
}
@Tag("b")
@Test
void test3() {
// ...
}
}
Entonces, si quisiéramos ejecutar solo pruebas que tengan la etiqueta «a», iríamos a Ejecutar -> Editar configuraciones y cambiaríamos los siguientes dos campos antes de ejecutar la prueba:
@RepetidoPrueba
Esta anotación funciona igual que la @Test
anotación, pero ejecuta el método de prueba el número de veces especificado. Cada iteración de prueba puede tener su propio nombre, mediante el uso de una combinación de marcadores de posición dinámicos y texto estático. Los marcadores de posición disponibles actualmente son:
{displayName}
: nombre para mostrar del@RepeatedTest
método{currentRepetition}
: el recuento de repeticiones actual{totalRepetitions}
: el número total de repeticiones
El nombre predeterminado de cada iteración es «repetición {currentRepetition} de {totalRepetitions}».
//@RepeatedTest(5)
@DisplayName("Repeated Test")
@RepeatedTest(value = 5, name = "{displayName} -> {currentRepetition}")
void rptdTest(RepetitionInfo repetitionInfo) {
int arbitrary = 2;
System.out.println("Current iteration: " + repetitionInfo.getCurrentRepetition());
assertEquals(arbitrary, repetitionInfo.getCurrentRepetition());
}
El RepetitionInfo
parámetro no es necesario, pero podemos acceder a él si necesitamos esos datos. Obtenemos una pantalla limpia con respecto a cada iteración cuando ejecutamos esto:
@ParametrizedTest
Las pruebas parametrizadas también permiten ejecutar una prueba varias veces, pero con diferentes argumentos.
Funciona de manera similar, por @RepeatedTest
lo que no volveremos a pasar por todo, solo por las diferencias.
Debe agregar al menos una fuente que proporcionará los argumentos para cada iteración y luego agregar un parámetro del tipo requerido al método.
@ParameterizedTest
@ValueSource(ints = {6,8,2,9})
void lessThanTen(int number) {
assertTrue(number < 10, "the number isn't less than 10");
}
El método recibirá los elementos de la matriz uno por uno:
Te puede interesar:Modificadores de acceso en Java@ValueSource
es solo un tipo de anotación que acompaña @ParametrizedTest
. Para obtener una lista de otras posibilidades, consulte la documentación .
@Anidado
Esta anotación nos permite agrupar las pruebas donde tenga sentido hacerlo. Es posible que deseemos separar las pruebas que tratan de la suma de las pruebas que tratan de la división, la multiplicación, etc. y nos proporciona un camino fácil para @Disable
ciertos grupos por completo. También nos permite probar y hacer oraciones completas en inglés como resultado de nuestra prueba, haciéndolo extremadamente legible.
@DisplayName("The calculator class: ")
class CalculatorTest {
Calculator calc;
@BeforeEach
void init() {
calc = new Calculator();
}
@Nested
@DisplayName("when testing addition, ")
class Addition {
@Test
@DisplayName("with positive numbers ")
void positive() {
assertEquals(100, calc.add(1,1), "the result should be the sum of the arguments");
}
@Test
@DisplayName("with negative numbers ")
void negative() {
assertEquals(100, calc.add(-1,-1), "the result should be the sum of the arguments");
}
}
@Nested
@DisplayName("when testing division, ")
class Division {
@Test
@DisplayName("with 0 as the divisor ")
void throwsAtZero() {
assertThrows(ArithmeticException.class, () -> calc.divide(2,0), "the method should throw and ArithmeticException");
}
}
}
@TestInstance
Esta anotación se usa solo para anotar la clase de prueba con @TestInstance(Lifecycle.PER_CLASS)
para decirle a JUnit que ejecute todos los métodos de prueba en una sola instancia de la clase de prueba, y no cree una nueva instancia de la clase para cada método de prueba.
Esto nos permite usar variables de nivel de clase y compartirlas entre los métodos de prueba (generalmente no se recomienda), como inicializar recursos fuera de un método @BeforeAll
o @BeforeEach
y @BeforeAll
ya @AfterAll
no es necesario static
. Por tanto, el modo «por clase» también hace posible el uso de métodos @BeforeAll
y @AfterAll
en @Nested
clases de prueba.
La mayoría de las cosas que podemos hacer @TestInstance(Lifecycle.PER_CLASS)
se pueden hacer con static
variables. Tenemos que tener cuidado de restablecer todas las variables que necesitaban restablecerse a un cierto valor en @BeforeEach
, que generalmente se restablecían cuando la clase se reiniciaba cada vez.
Supuestos
Además de las afirmaciones antes mencionadas, tenemos supuestos. Cuando una suposición no es cierta, la prueba no se ejecuta en absoluto. Las suposiciones se utilizan normalmente cuando no tiene sentido seguir ejecutando una prueba si no se cumplen determinadas condiciones, y la mayoría de las veces la propiedad que se está probando es algo externo, no directamente relacionado con lo que estamos probando. Hay algunos métodos de suposición sobrecargados:
assumeTrue(boolean assumption, optionalMsg)
yassumeFalse(boolean assumption, optionalMsg)
solo ejecutará la prueba si lo proporcionadoassumption
es verdadero y falso, respectivamente. SeoptionalMsg
mostrará solo si la suposición no es cierta.assumingThat(boolean assumption, Executable exec)
– siassumption
es verdadero,exec
se ejecutará; de lo contrario, este método no hace nada.
Se BooleanSupplier
puede usar A en lugar de regular boolean
.
class CalculatorTest {
Calculator calc;
boolean bool;
@BeforeEach
void init() {
System.out.println("inside @BeforeEach");
bool = false;
calc = new Calculator();
}
@Test
void additionTest() {
assumeTrue(bool, "Java sees this assumption isn't true -> stops executing the test.");
System.out.println("inside additionTest");
assertAll(
() -> assertEquals(2, calc.add(1,1), "Doesn't add two positive numbers properly"),
() -> assertEquals(0, calc.add(-1,1), "Doesn't add a negative and a positive number properly"),
() -> assertNotNull(calc, "The calc variable should be initialized"));
}
@Test
void divisionTest() {
assumeFalse(0 > 5, "This message won't be displayed, and the test will proceed");
assumingThat(!bool, () -> System.out.println("uD83DuDC4C"));
System.out.println("inside divisionTest");
assertThrows(ArithmeticException.class, () -> calc.divide(2,0));
}
}
Lo que nos daría la salida:
inside @BeforeEach
👌
inside divisionTest
inside @BeforeEach
org.opentest4j.TestAbortedException: Assumption failed: Java sees this assumption isn't true -> stops executing the test.
Conclusión y consejos
La mayoría de nosotros probamos el código ejecutando manualmente el código, ingresando alguna entrada o haciendo clic en algunos botones y verificando la salida. Estas «pruebas» suelen ser un escenario de caso común y un montón de casos extremos en los que podemos pensar. Esto está relativamente bien con proyectos pequeños, pero se vuelve completamente un desperdicio en algo más grande. Probar un método en particular es particularmente malo: o System.out.println()
el resultado y lo verificamos, o lo ejecutamos a través de algunas if
declaraciones para ver si se ajusta a la expectativa, luego cambiamos el código cada vez que queremos verificar qué sucede cuando pasamos otros argumentos al método . Analizamos visual y manualmente en busca de algo inusual.
JUnit nos brinda una forma limpia de administrar nuestros casos de prueba y separa la prueba del código del código en sí. Nos permite realizar un seguimiento de todo lo que debe probarse y nos muestra lo que no funciona de manera ordenada.
Te puede interesar:Cómo enviar solicitudes HTTP en JavaGeneralmente, desea probar el caso común de todo lo que pueda. Incluso los métodos simples y directos, solo para asegurarse de que funcionan como deberían. Esta podría incluso ser la parte más importante de las pruebas automatizadas, ya que cada vez que cambia algo en su código o agrega un nuevo módulo, puede ejecutar las pruebas para ver si ha roto el código o no, para ver si todo sigue funcionando. como lo hizo antes de la «mejora». Por supuesto, los casos extremos también son importantes, especialmente para métodos más complejos.
Siempre que encuentre un error en su código, es una muy buena idea escribir una prueba antes de solucionar el problema. Esto asegurará que si el error se repite, no necesitará perder tiempo averiguando qué salió mal nuevamente . Una prueba simplemente fallará y sabrá dónde está el problema.