La popularización de lenguajes como Ruby y JavaScript de lado de servidor, ha generado en los últimos años nuevos frameworks de pruebas centrados en lo que se denomina pruebas de especificación, en la cual probamos nuestro código utilizando siempre los casos de uso de dominio, de una manera visual, herramientas como como RSpec, Mocha, o Jest, que siguen una estructura muy similar a la mostrada a continuación.
Supongamos que queremos probar una estructura de tipo Stack:
describe "A Stack"
describe "pop"
it "should get the item from the top of the stack"
//CODE
it "should get null if stack is empty"
//CODE
Esta manera de hacer pruebas contrasta con el estilo que podemos ver en lenguajes como Java en JUnit:
class StackTests
void test_when_pop_get_item_from_top
//CODE
void test_when_pop_get_null_if_empty
//CODE
Aunque son ejemplos muy sencillos, al aumentar la complejidad de nuestra base de código, aumentan también los casos de uso que manejamos, y por tanto el número de tests. Para ello, el estilo de las pruebas de especificación nos permiten agregar casos adicionales reduciendo duplicidad y mejorando la legibilidad.
En el artículo de hoy veremos un framework que nos permitirá hacer este tipo de pruebas en Java, llamado Specnaz y creado por Adam Ruka.
Para este ejemplo utilizaré la kata bowling definida aquí: http://kata-log.rocks/bowling-game-kata.
Nuestro primer test
Para nuestro primer test definimos la primera condición de nuestro juego, que es que un juego vacío devuelva 0 como primer resultado.
...
public class BowlingSpec extends SpecnazJUnit {
{
describes("A Game", it -> {
Game game = new Game();
it.should("show 0 as score when first created",
() -> assertThat(game.getScore()).isEqualTo(0));
});
}
}
Nuestro objeto "Game" tendrá este aspecto inicialmente:
...
public class Game {
public int getScore() {
return 0;
}
}
Inicializadores
Una de las cosas que podemos hacer en nuestros tests, es crear una inicialización antes de cada test, así como una limpieza, esto lo podemos conseguir utilizando el comando de beginsEach como se muestra en el ejemplo:
...
public class BowlingSpec extends SpecnazJUnit {
{
describes("A Game", it -> {
Game game = new Game();
it.beginsEach(game::reset);
it.should("show 0 as score when first created",
() -> assertThat(game.getScore()).isEqualTo(0));
});
}
}
En vez de crear una instancia nueva de Game, en este ejemplo creamos un método de inicialización dentro del objeto Game que reinicia el estado de la partida. Debido a que estamos en el contexto de una lambda, no podemos redefinir el valor de una variable externa, con lo cual ejecutar el constructor repetidamente no funcionaría
Creando ramas en nuestros tests
Una de las características más interesantes que nos aportan este tipo de tests es la posibilidad de crear ramas con las diferentes condiciones de prueba sobre las que ejecutamos nuestros tests. Veamos un ejemplo:
describes("A Game", it -> {
...
it.describes("when throwing a spare", () -> {
it.should("account for the number of pins knocked down by next roll", () -> {
game.roll(4);
game.roll(6);
game.roll(4);
game.roll(0);
assertThat(game.getScore()).isEqualTo(18);
});
});
});
Dentro de cada grupo de "describes" podemos además definir nuestras propias secuencias de inicio y fin de cada test, creando un árbol de casos de uso y evitando repetición innecesaria.
Usando parámetros
En el caso de la kata bowling, una manera muy útil de agrupar los tests es utilizando parámetros, y para ello podemos utilizar una construcción de Specnaz llamada SpecnazParamsJUnit en el caso de que estemos utilizando JUnit como motor de ejecución de tests.
En este ejemplo podemos ver además cómo especificamos los parámetros, y cómo en la cabecera should podemos asignar valores a parámetros:
public class BowlingParamsSpec extends SpecnazParamsJUnit {
{
describes("A Parametrized Game", it -> {
Game game = new Game();
it.beginsEach(game::reset);
it.should("properly calculate the gameplays of %1 equal %2", (List<Integer> rolls, Integer expected) -> {
rolls.forEach(game::roll);
assertThat(game.getScore()).isEqualTo(expected);
}).provided(
p2(Arrays.asList(2, 3), 5),
p2(Arrays.asList(4, 6, 4, 0), 18),
p2(Arrays.asList(2, 4, 6, 3), 15),
p2(Arrays.asList(10, 3, 6), 28),
p2(Arrays.asList(10, 10, 10, 0, 0), 60),
p2(Arrays.asList(0, 0, 10, 10, 10), 60),
p2(Arrays.asList(10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10), 300)
);
});
}
}
El resultado en nuestra ventana de tests será el siguiente:
should properly calculate the gameplays of [2, 3] equal 5
should properly calculate the gameplays of [4, 6, 4, 0] equal 18
should properly calculate the gameplays of [2, 4, 6, 3] equal 15
should properly calculate the gameplays of [10, 3, 6] equal 28
should properly calculate the gameplays of [10, 10, 10, 0, 0] equal 60
should properly calculate the gameplays of [0, 0, 10, 10, 10] equal 60
should properly calculate the gameplays of [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] equal 300
Manejando excepciones
No es raro que tengamos que encontrarnos con situaciones más o menos excepcionales cuando creamos nuestros tests, y eso incluye ciertos casos de prueba. Para probar excepciones podemos recurrir a shouldThrow que nos permite controlar el tipo de excepción que lanzamos:
it.shouldThrow(IllegalArgumentException.class, "when trying to roll more than 10 bowls in a single roll", () -> {
game.roll(11);
});
El resultado incluirá además el detalle del tipo de excepción que estamos intentando controlar en el mensaje que se muestra por la consola:
should throw IllegalArgumentException when trying to roll more than 10 bowls in a single roll
Tanto el ejemplo con parámetros como el ejemplo que acabamos de ver nos libran de cometer errores en la cabecera de los tests, ya que se define la excepción que estamos probando así como los parámetros, de tal manera que si cambiaran, cambiaría el nombre del test.
Conclusiones
Como hemos visto durante el artículo, Specnaz es un framework de pruebas de especificación que nos permite definir nuestras pruebas de una manera robusta, evitando duplicidades y permitiendo escribir con detalle qué estamos probando y bajo qué especificaciones.
Además de los ejemplos de este artículo, en su repositorio de github se pueden ver ejemplos de uso con herramientas como mockito para generar dobles de test y poder simular el comportamiento de otras clases, ya que en raras ocasiones probaremos código que no tenga dependencias.
Puedes aprender más detalles en los siguientes enlaces:
- Repositorio de github: https://github.com/skinny85/specnaz
- Artículo original en el blog de Adam: https://www.endoflineblog.com/specnaz-my-java-testing-library
- Repositorio con los ejemplos mostrados en este blog: https://github.com/rlbisbe/specnaz-bowling