Una manera diferente de hacer tests en Java con Specnaz

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:

Las 5 maneras en las que hago pruebas con ASP.NET

Al desarrollar aplicaciones en ASP.NET, tener un buen conjunto de pruebas es la diferencia entre encontrarte un fallo en desarrollo o en producción, así de sencillo. Las pruebas no evitan todos los errores, pero al menos nos permite que los caminos críticos se mantengan estables. Veamos de qué manera podemos probar nuestras aplicaciones ASP.net

1. Pruebas Unitarias

En una prueba unitaria probamos habitualmente los métodos y el comportamiento de una clase, aunque, por supuesto, no es necesario probar TODOS los métodos (getters y setters, por ejemplo, a menos que tengamos algun tipo de lógica de validación dentro de los mismos).

En una prueba unitaria, además, es importante que mantengamos aislada la clase a probar, de tal manera que todos los servicios o clases externas puedan ser simulados mediante mocks o clases de test específicas. Para estos mocks podemos recurrir a librerías como Moq.

Backend

Personalmente he utilizado tanto MSTest, NUnit como XUnit, estando más familiarizado con el primero ya que lo uso a diario. En general cada framework tiene sus características que lo hacen más interesante, aunque todo se reduce a encontar aquel con el que más cómodos nos sintamos.

Frontend

Si nuestra página contiene lógica de lado de cliente (por ejemplo, si estamos diseñando una SPA con Angular, Knockout o React) se hace prácticamente imprescindible el uso de tests. Para ello podemos recurrir a frameworks como QUnit, Jasmine o Mocha, herramientas que nos permiten establecer unas condiciones para crear tests, así como proporcionarnos un entorno de ejecución para ejecutar las mismas.

Estas herramientas, además, se pueden integrar con Visual Studio y también con Team Foundation Server/Visual Studio Online mediante la extensión Chutzpah que podemos agregar a nuestra solución.

2. Pruebas de integración

En una prueba de integración probamos la interacción entre diferentes módulos de nuestra aplicación, y en este caso el objetivo es asegurarnos que ciertos caminos funcionan correctamente entre las diferentes capas de la misma. Una prueba de aceptación no tiene por qué cubrir todo el sistema, sino que puede cubrir tan solo una parte del mismo.

Un ejemplo podría ser comprobar que el valor que llega a un controlador, pasa por la capa que valida la lógica de negocio de ese valor y finalmente devuelve una respuesta correcta, simulando la inserción en la base de datos.

Mientras más fuertes sean nuestras pruebas de integración entre componentes del sistema, más facilidad tendremos para encontrar y corregir posibles errores de regresión que no hayamos encontrado en pruebas unitarias, ya que, habitualmente, es en la interacción en donde pueden surgir más problemas inesperados.

3. Pruebas de UI

Una de las cosas que más problemas nos puede dar es no saber si los caminos críticos de nuestra web están funcionando correctamente, y para ello, la única manera de poder asegurarnos al 100% es mediante una prueba que simule un usuario navegando por la página. Para este cometido podemos utilizar Selenium, que nos permite grabar el comportamiento de un usuario en nuestra página y posteriormente reproducirlo, así como escribir este comportamiento en C# y ejecutarlo como si de un test de integración se tratara.

4. Pruebas de carga

¿Qué pasa cuando la aplicación recibe 1000 usuarios de golpe? ¿Soportará un «efecto menéame?. Benditos problemas, pero problemas al fin y al cabo. Si estamos trabajando en una solución que tenga que soportar cierta carga (prensa, bancos, redes sociales…) las pruebas de carga deben formar parte de nuestro plan de pruebas, ya que nos permitirán estresar el sistema y poder descubrir otro tipo de errores. Una de las herramientas para realizar estas pruebas de carga es JMeter del proyecto Apache, que nos permite además personalizar la salida de estas pruebas y poder ver los resultados como listas o como gráficas.

5. Tests exploratorios

Este tipo de pruebas resulta bastante interesante y ya hemos hablado de ellas en alguna ocasión ya que mezclan intuición y experiencia adquirida, para intentar encontrar nuevos fallos, errores o escenarios que se escapen del uso habitual del sistema (los denominados también «Corner cases»).

La ventaja de estos tests es que nos aportan un mayor conocimiento sobre el sistema, aunque pueden resultar difíciles de realizar o incluso tediosos, aunque hemos de pensar que no estamos haciendo el trabajo de la máquina, sino intentando ir un poco más allá.

Conclusiones

Aunque los test exploratorios pueden caer fuera del ciclo habitual de test de una tarea, no es conveniente dejarlos fuera, ya que nos pueden aportar mucho conocimiento sobre una herramienta, sobre todo si tenemos que trabajar con código heredado.

Estas son solo cinco opciones que personalmente uso o he usado en mi día a día, existen más sabores y tipos de tests, aunque el objetivo de todos es el mismo, asegurar la fiabilidad del código que estamos poniendo en producción.

¿Y tú, qué pruebas haces? Déjame un comentario o hablemos en @rlbisbe

8 cosas aprendidas de Android en el Codemotion 2014

En el pasado Codemotion 2014 pude asistir a varias charlas relacionadas con el desarrollo de aplicaciones para Android, en las que pude aprender y recordar algunos conceptos y herramientas que resumo en este pequeño artículo:

1. Model-View-Presenter

Fuente: Wikipedia DE
Fuente: Wikipedia DE

El patrón Model View Presenter es muy similar a Model View ViewModel en la base, ya que tanto el ViewModel como el Presenter notifican a la vista de los cambios, y reciben los comandos de la misma.

En el caso de Android, se propone el uso de Interfaces entre Presenter y la Vista, de manera que trabajamos, con clases abstractas, consiguiendo cierta independencia con las implementaciones.

2. Arquitecturas hexagonales

El uso de las clases abstractas con los MVP es lo que deriva en una arquitectura hexagonal, en la que mediante puertos (que es como se denominan estas interfaces) nuestra lógica de negocio se comunica con las diferentes abstracciones de la vista y diferentes servicios como comunicaciones o almacenamiento.

Fuente: VICTOR SAVKIN
Fuente: VICTOR SAVKIN

En el caso concreto de Android, la lógica de negocio puede estar completamente aislada en un módulo Java puro, lo que da una mayor independencia de la plataforma.

3. Documentación de calidad mantenida por la comunidad

android-guides

Los manuales de android guides, creados por los chicos de codepath, contienen (a fecha de hoy) más de 100 artículos con guías prácticas de desarrollo, cubriendo temas como fragments, persistencia, o guías completas como cómo empezar con gradle. Resultan verdaderamente interesantes y desde luego es un lugar a añadir a mi lista de favoritos.

4. Motores de Inyección de dependencias

De la misma manera que tenemos ninject en .NET o bien podemos crear nuestro propio motor, para Android tenemos Dagger (para inyección de dependencias en general) y Butterknife (para inyectar vistas), que nos permiten gestionar las dependencias de nuestros objetos (para implementar una arquitectura hexagonal como la que hemos definido anteriormente, por ejemplo).

5. Bases de datos y almacenamiento

url

En función de las necesidades que tengamos, hay dos proyectos que deberíamos tener en cuenta:

  • SugarORM, permite tener una base de datos local de manera extremadamente simple y manejable.
  • ORMLite compatible con cualquier proyecto de Java y permite una mayor personalización y estructuras un poco más complejas, además de estar más extendido en la comunidad.

Además, podemos o bien utilizar la clase ContentProvider para almacenar datos, o bien usar los adaptadores propios del sistema operativo, aunque se recomiendan las dos opciones anteriores.

Web: SugarORM (no confundir con SugarCRM): http://satyan.github.io/sugar/
Web: ORMLite: http://ormlite.com/

6. Testing

Para desarrollo Android tenemos muchas herramientas para hacer testing, desde JUnit que nos permite además de código Java estándar probar código específico para la plataforma, pasando por Mockito para crear artefactos para nuestros tests, y acabando con Espresso, para probar la UI de nuestra aplicación.

7. Programación reactiva

La programación reactiva o Reactive Programming es un paradigma que se basa en el flujo de datos, actualizando la visualización de los mismos a través del envío y recepción de mensajes. Es un tema verdaderamente interesante, ya que mezcla conceptos de programación funcional con patrones de lenguajes orientados a objetos como observer.

8. Otros lenguajes

Uno de los puntos fuertes de la JVM es la capacidad para ejecutar otros lenguajes más allá de Java, de esta manera podemos usar lenguajes como Kotlin (creado por Jetbrains), Groovy o incluso Clojure para hacer aplicaciones Android. El soporte para otros lenguajes es algo que está en proceso, pero que podemos tener en cuenta (Android Studio se lleva bastante bien con Kotlin, por ejemplo). Si nos vamos fuera de la JVM tenemos opciones como Xamarin que nos permite desarrollar apps para Android usando C#.

Conclusiones

Esta lista es solamente un resumen de varias charlas y conversaciones tenidas durante el evento. El ecosistema Android es muy interesante, no solo por todas las herramientas y técnicas disponibles, que una vez aplicadas se pueden llevar a otras plataformas, sino además por la comunidad que tiene, no solamente Android sino el lenguaje Java y los lenguajes basados en la JVM.

Nos vemos en la siguiente, aquí te dejo un recopilación de todas las charlas del Codemotion: https://docs.google.com/spreadsheets/d/14ZjJQ_VeT8mKmO8MANA1Os5zwRA3Pu_rpFdfyFtBZ4A/edit#gid=0