🇺🇦 Слава Україні! Consulta cómo puedes ayudar a Ucrania desde España u otros países en supportukrainenow.org.

JUnit 4

Excepciones con expected y límites de tiempo con timeout

• Duración: 5:57 • #java #testing #junit #unit-testing

Dos propiedades de la anotación Test que nos permiten hacer pruebas avanzadas para algunos casos especiales. Con Expected podemos comprobar que dentro de un test se tira una excepción. Si le ponemos este atributo a una anotación Test, el test fallará si alcanza el final de la función y no se ha lanzado ninguna ocurrencia de la excepción proporcionada, y es el método que usaremos en caso de querer probar excepciones. Por otro lado, timeout es un parámetro que podemos usar para forzar al test a fallar si supera un tiempo determinado de ejecución.

En esta lección analizaremos dos parámetros que se pueden proporcionar a la anotación @Test. Normalmente cuando hacemos un test escribimos @Test antes de la signatura de la clase. Si nunca has trabajado con anotaciones más allá de esto o del @Override, tal vez esto sea todo lo que sepas sobre anotaciones.

Sin embargo, las anotaciones pueden tener parámetros. En ese caso las escribimos entre paréntesis indicando el nombre de cada parámetro, luego un igual, y luego el valor del argumento:

@Test(expected = ArithmeticException.class, timeout = 5000)
public void testComplejo() {
    // . . .
}

expected: probar excepciones

En primer lugar, el parámetro expected lo podemos usar para comprobar que el test deba lanzar excepciones. Generalmente, si un test lanza una excepción, se marca como inválido, porque por defecto JUnit va a asumir que el propósito del test es ejecutarse de forma satisfactoria.

Esto no se corresponde con lo que obtenemos cuando probamos un caso límite, o cuando probamos precisamente los casos negativos de una función, como probar que en una calculadora se obtiene un error si intentamos dividir por 0. Podemos intentar hacer esto a mano usando un try-catch y aprovechándonos del aserto fail:

@Test public void testDividirPorCero() {
    Calculadora calc = new Calculadora();
    try {
        calc.div(5, 0);
        fail("Si este fail se ejecuta, tu calculadora no falla");
    } catch (ArithmethicException e) {
        // El test está correcto.
    }
}

Sin embargo, una forma más clara sería únicamente usar el parámetro expected para indicar que el test debe fallar.

@Test(expected = ArithmeticException.class)
public void testDividirPorCero() {
    Calculadora calc = new Calculadora();
    calc.div(5, 0);
}

En este caso, JUnit comprobará que el test lanza una excepción de la clase que le proporcionemos como valor. Si el test no lanza ninguna excepción durante su ejecución, JUnit forzará un error indicando que se esperaba una excepción siendo lanzada pero que no tuvo que capturar nada.

Observa también como JUnit te exige que pases tal cual un valor de tipo Class. Deberás utilizar el atributo estático .class, que sirve para obtener tal cual la instancia Class<?> de una clase.

timeout: acotar un test en el tiempo

Este otro parámetro puede ser de utilidad cuando lo que queremos es limitar el tiempo que un test puede permanecer en ejecución. Con timeout podemos especificar un valor en milisegundos que le vamos a dar como límite al test para que termine. Si el test permanece más milisegundos de lo permitido en ejecución, entonces JUnit interrumpirá la ejecución del test y lo marcará como erróneo.

@Test(timeout = 2000)
public void testTemporal() {
    // Este test no debería fallar.
    Calculadora calc = new Calculadora();
    calc.div(5, 2);
}

Podemos usar esto para limitar el tiempo en ejecución de un test. Algunos ejemplos de cuando esto nos puede venir bien:

  • Podemos usar timeout para garantizar que un algoritmo no se vuelve poco óptimo con el tiempo. Imagina que haces un cambio a un método de una clase auxiliar y eso provoca que el rendimiento del algoritmo de cálculo principal se degrade. Si ponemos un timeout al test que ejecuta los algoritmos, podemos detectar cuando hemos hecho por accidente que el rendimiento de nuestro programa empeore. Se trata de un test de regresión.

  • En algunas ocasiones, vamos a hacer tests que no son unitarios, sino de integración. Por ejemplo, puede que un test tenga que conectarse con un servidor o base de datos externa a fin de obtener un parámetro para otro test. En caso de que ese servidor o base de datos falle, tal vez se ralentice toda la ejecución del test. Con un timeout podemos reducir el tiempo que hay que esperar para marcar todo el test como inválido si no se puede establecer una conexión con el servidor.

  • En algunos casos más extremos, podemos usar el timeout para protegernos ante bucles infinitos que nos salten por error de vez en cuando en tests que todavía no sabemos por qué fallan.

@Test(timeout = 2000)
public void testInfinito() throws InterruptedException {
    Thread.sleep(4000);
}

En este caso, el test sólo demorará 2 segundos en ejecutarse, incluso aunque dentro hayamos puesto un sleep() de 4 segundos, debido a que tras 2 segundos JUnit parará la ejecución del test.

Desplegar transcripción del episodio

Hola a todos, qué tal como estáis y bienvenidos al episodio número 5 de este tutorial de JUnit. En el episodio de hoy os voy a contar un par de secretos que se guarda la anotación Test y que os van a venir bastante bien a la hora de realizar algunas pruebas unitarias.

Y es que Test es una anotación que tiene parámetros, y estos dos parámetros son expected y timeout, que en el vídeo de hoy vamos a ver más o menos en qué consiste.

En primer lugar os voy a hablar acerca de expected. expected es un parámetro que puede usarse para indicar que un método se espera que tire una determinada excepción. De este modo lo que conseguimos es que una prueba unitaria falle en el caso de que no se tire una excepción que se supone debería tirarse.

Por ejemplo, pongamos que yo quiero agregar un método llamado dividir que haga lo que podéis imaginaros: ans va a ser igual a a / b, y luego voy a devolver el resultado de ans. Esto no está del todo correcto, ¿vale? porque sí, yo puedo hacer una prueba que se sea testDividir, y recordar que por cómo funcionaba el Before, ya tengo mi calculadora inicializada.

Imaginemos que yo divido 5 entre 2. Pues esto, evidentemente, va a funcionar. Si yo corro mi prueba unitaria. Todo sale de color verde, todo perfecto, todo maravilloso. Ahora bien, pongamos que yo hago una división de cinco partido por cero. Ya sabéis que no se puede dividir nada entre cero.

Esto va a provocar un error... me gustaría quitar la consola, la verdad, que molesta un poco a la hora de estar explicando cosas... vale decía.

Esto va a tener va a tirar un error porque se genera una excepción que no se supone debería generarse. Entonces, lo que necesitamos es tratar más o menos ese caso, y evidentemente esto lo tenemos que hacer con algún tipo de catch, por ejemplo, si b es igual a cero pues tiramos un nuevo ArithmeticException, creo que se llamaba... ArithmeticException: no puedes dividir por cero. Es imposible.

A partir de aquí, ya podemos hacer nuestra prueba, por ejemplo, diciendo vale, esto lo dividimos bien, pero ahora imaginemos que yo quiero hacer una división por cero, y quiero comprobar que al llamar a calc.div(5, 0) se genera un error. Bueno, pues aquí lo que tendré que hacer es hacer mi prueba y la hora de diseñar mi prueba, poner expected es igual a, y el nombre de la clase con la excepción que se supone debe tirarse. Por ejemplo, en este caso ArithmeticException.class, no podéis poner ArithmeticException a secas porque no queda bien, tenéis que poner el .class. Entonces, esta prueba funcionará porque se espera una división por cero, y cuando se espera una división por cero se espera que salte un error, y el expected básicamente viene decir eso mismo. Por lo que ahora como veis la prueba sale correctamente.

Y en el hipotético caso de que no se tirase una excepción, por ejemplo porque no divida partido de cero, sí se va a mostrar una excepción. No sé por qué no se está mostrando la lista de métodos, pero bueno es que debido tocar algo... showFailures... vale, ya está. Se muestra un error básicamente porque se esperaba un ArithmeticException, que no se tira en ningún momento. Entonces, tenéis métodos por ahí que se supone que hay que tratar para casos límite, argumentos inválidos, cosas que no deberían entrar, cosas que no deberían salir, parámetros inválidos, situaciones erróneas... todo ese tipo de situaciones, si queréis comprobar que se tire una excepción y que no se ejecuta de alguna manera, utilidad expected. Recordad que un expected siempre va a fallar en el caso de que no se tiene una excepción que le indiquéis.

Eso por un lado. Os voy a hablar del otro secretito que guarda test que es timeout. El timeout es un parámetro interesante, de hecho es un parámetro que me mosquea no haber conocido antes, lo había descubierto mirando la documentación para poder preparar estos vídeos. Es un parámetro que sirve para hacer que una prueba falle si un método se tira más tiempo de lo normal haciendo su prueba. Y éste me hubiese venido bien conocerlo en su momento, la verdad, así que para que no cometáis el mismo error que yo, os lo voy a contar.

Imaginemos que tenemos un método que tarda más de lo normal, y queremos evitar esa situación. Es un poco raro este tipo de situación en el que queramos controlar el tiempo que se ejecuta un método, pero bueno, puede darse en algún momento, de hecho yo digo que yo tuve que montar un pifostio el año pasado para poder hacer una situación así poder controlar el tiempo que tardaba en ejecutarse... y bueno, básicamente esto lo que nos permite es detener la ejecución cuando una función tarda mucho tiempo en ejecutarse. He encontrado dos ejemplos de uso... vamos, he encontrado... se me han ocurrido pero que no sé si se puede aplicar realmente en la vida real, o al menos así normalmente.

Imaginemos un método que tiene que hacer una operación muy larga, ¿vale? Una operación óptima, más bien, que se supone que debe hacer un algoritmo en un intervalo de tiempo, que no debe tardar mucho porque si tarda mucho es que no está optimizado y ese tipo de historias...

Bueno mi algoritmo es un poco justo y tarda 2 segundos en ejecutarse, y se supone que no debería tardar tanto tiempo en ejecutarse: con 100 milisegundos debería bastar. Bueno, pues yo puedo crear una prueba de este algoritmo óptimo, ¿vale? En el que diga, a ver llámame a la operación óptima, y como es una operación que es óptima no debería tardar en ejecutarse el cálculo más de 100 milisegundos, pongamos. Entonces, le pasamos al valor de milisegundos, y siempre milisegundos, y en el hipotético caso de que se produzca un error porque tarde más de lo normal pues se va a detener la prueba automáticamente y se va a generar un error.

Tengo comprobado que este método dispara cuando se lanza la excepción porque se tarda mucho tiempo un InterruptedException, es decir que si yo aquí pongo cualquier mensaje no lo voy a hacer porque es que tengo una consola en la otra punta de la ventana y es un poco aburrido, será ejecutar este bloque porque estamos interrumpiendo el hilo básicamente así que evidentemente esto se tira. En otro caso en el que me he dado cuenta que viene bastante bien es, por ejemplo, para evitar bucles infinitos, por absurdo que parezca. Imaginemos que no tenemos un caso tan exagerado como éste, ¿vale? Pero imaginemos que tenemos un algoritmo que hemos hecho mal y que entra en un bucle infinito en según qué casos. Bueno pues, debido a que en JUnit las pruebas corren en hilos distintos, podemos interrumpir la prueba en el caso de que lleve demasiado tiempo. Esto viene bien, pues eso, para evitar bucles infinitos, que es donde me da cuenta que también viene bastante bien.

Pero vaya que si tenéis imaginación pues seguramente se os ocurran ideas mejores que la mía. Aun así esto es lo que quería contaros en este vídeo: expected y timeout. Son dos parámetros que hay en la anotación Test, que vienen bastante bien para hacer pruebas unitarias avanzadas.