QuickCheck en la JVM

Como muchos otros techies a mi me encanta probar cosas nuevas y ahora mismo hemos acabado uno de los MOOC relacionado con Scala. En estos cursos se suele aprender muchas cosas, y colateralmente estoy aprendiendo una cosa que creo que puede ser de interés son las herramientas basadas en QuickCheck que son de esas cosas que nos pueden servir en un momento dado.

Os pongo en antecedentes:

El problema

Como podréis imaginar por los posts publicados en soft.war.fair creemos firmemente en la calidad del software basándose en los tests. Pero cuando te enfrentas a los tests, por ejemplo usando TDD, suele aparece una duda existencial que es: ¿cuál es el juego de prueba que tengo que usar? Y en especial, ¿Hasta donde tengo que escribir códigos de prueba? Obviamente suele haber un tradeoff a la hora de elegir nuestra batería de tests, se suele empezar por los casos base y después extenderlo con casos más complejos, pero ¿realmente estamos soportando todos los posibles casos? Seguramente no, aunque en muchas ocasiones no es importante en otras si lo es. Además para poder soportar más casos tenemos que escribir más y más código de pruebas, que al final hace que nuestra base de código crezca de manera importante.

La solución

Pues bien, en el entorno del lenguaje funcional Haskell desde hace mucho tiempo (desde el 99!) existe QuickCheck que básicamente lo que sirve para generar propiedades que representan aquello que queremos verificar y la herramienta se encarga de validarlo. Estas propiedades potencialmente pueden testear todos los posibles casos, obviamente esto puede ser infinito en tiempo según el tipo de dato pero al menos buscan diferentes distribuciones generalmente aleatorias para cubrir el mayor el mayor número de posibilidades. Otra cosa importante es que se suele simplificar el código y además se suelen probar casos extremos (esto según la herramienta) de manera que no tenemos que preocuparnos mucho más allá.

El ejemplo

Para este caso iremos a una cosa muy sencilla, simplemente tenemos un rectángulo y probamos que cualquier rectángulo válido cumple unas propiedades como por ejemplo que se genera bien el área o la hipotenusa. Con este ejemplo podemos ver la potencia directamente, pero pensad que la gracia está en poder probar con muy poco código propiedades más complejas, como la posibilidad de poder trabajar con varios rectángulos.

Este será el código que queremos testear:

public class Rectangle {
    private int height;
    private int width;
	…
    public long area(){
        return height * width;
    }

    public boolean biggerThan(Rectangle that) {
        return area() > that.area();
    }

    public void add(int n) {
        validate(height +n, width +n);
        this.height = height +n;
        this.width = width +n;
    }
}

Y aquí podéis ver como serían algunos test al estilo habitual, probando casos base y después algún caso más genérico:

    @Test
    public void testAreaEmpty1() throws Exception {
        Rectangle cut = new Rectangle(0,0);
        Assert.assertEquals(0l, cut.area());
    }

...

    @Test
    public void testAreaCorrectOneXTwo() throws Exception {
        Rectangle cut = new Rectangle(1,2);
        Assert.assertEquals(2l, cut.area());
    }

Primer candidato: jUnit-quickcheck

En este caso, como su nombre indica, sólo se puede utilizar con el framework jUnit. Se basa en anotaciones y permite ejecutar un test que recibe parámetros muchas veces con dichos parámetros diferentes siguiendo un patrón. Es algo parecido a los parametrized tests, pero bastante más potente y conciso. Los tipos de datos que se pueden usar como parámetros son principalmente tipos primitivos y algunas collections, pero se puede ampliar con tus propios tipos aunque es un poco engorroso.

También puedes configurar los parámetros para que sean con unos ciertos límites que funciona muy bien con los tipos predefinidos.

Veámos un ejemplo

@Theory
    public void testAreaFineGrained(@ForAll @InRange(minInt = 0, maxInt = 300) int x,
                                    @ForAll @InRange(minInt = 0, maxInt = 300) int y) throws Exception {
        Rectangle out = new Rectangle(x,y);
        Assert.assertEquals(x*y, out.area());
    }

    @Theory
    public void testNotIsSquare(@ForAll @InRange(minInt = 0) int x,
                                    @ForAll @InRange(minInt = 0) int y) throws Exception {
        Assume.assumeThat(x, is(not(y)));
        Rectangle out = new Rectangle(x,y);
        Assert.assertFalse(out.isSquare());

    }

En el primer caso estamos generando los parámetros entre 0 y 300, el framework por defecto hará 100 ejecuciones aunque esto se puede configurar.

Mientras en el segundo caso aparece la necesidad de asegurarse que no sean iguales, y la manera de hacerlo es con las asunciones de jUnit, pero con el riesgo de que si en ninguna de las ejecuciones lanzadas se cumple la asunción el test fallará por falta candidatos.

Segundo candidato: QuickCheck

Si, el nombre es muy original :P. En este caso es agnóstico del tipo de test que estemos haciendo y como veréis es más tedioso de construir/configurar pero algo más potente.

En este caso la idea es tener (algunos ya implementados) generadores de objetos, pudiendo componer los generadores para por ejemplo construir objetos más complejos. Aquí podemos ver un generador de Rectángulos:

class RandomRectangleGenerator implements net.java.quickcheck.Generator{
    Generator x = PrimitiveGenerators.positiveIntegers();
    Generator y = PrimitiveGenerators.positiveIntegers();

    @Override public Rectangle next() {
        return new Rectangle(x.next(), y.next());
    }
}

Después para usarlo puede ser tan simple como esto:

  @Test
    public void testArea() throws Exception {
        for (Rectangle rectangle : Iterables.toIterable(new RandomRectangleGenerator())) {
            Assert.assertEquals(rectangle.getHeight() * rectangle.getWidth(), rectangle.area());
        }
    }

Se supone que deben funcionar también las anotaciones pero creo que no funciona del todo bien.

Esta herramienta tiene cosas interesantes, como la generación aleatoria basada en un semilla fija. Y vosotros diréis, lo cualo??? Pues me refiero a que aunque se generen enteros aleatorios para poder cubrir de manera significativa el espectro de los enteros, los enteros que se van a generar serán siempre los mismos, por lo que tendremos cierta predictibilidad útil a veces para, por ejemplo, ejecutar tests en servidores de integración continua. A esto se le llama generación determinista. También tiene generación según estratégias: sólo valores únicos, basado en transformaciones, excluyendo valores, etc.

Esta herramienta es sin duda útil para poder tener nuestros generadores de objetos de manera rápida y sencilla.

Tercer y último (por ahora) candidato: ScalaCheck

En este caso, como ya he comentado antes, se trata de una librería para ser usada en Scala, claro que entonces alguno pensará que no se pueden testear clases java, pero no es así pues todo Scala se transforma a una clase Java y por tanto la interoperabilidad es casi transparente (a excepción de algunos tipos de datos).

En el caso de ScalaCheck también se basa en el concepto de generadores pero algo más avanzado y libre a la vez. Al tratarse de un lenguaje funcional está muy orientado a tener propiedades a verificar y permitir tener un modelo muy cercano al lenguaje natural. Una propiedad puede ser un tipo de verificación con una serie de datos aleatorios, con la gracia que la librería te permite “unir” propiedades por ejemplo diciendo que se tienen que validar todas o al menos una de las propiedades, etc.

Vamos a ver un ejemplo sin utilizar aún un generador nuestro:

 property("area") = forAll { (x:Int, y:Int) =>
    (x >= 0 && y >= 0) ==> {
      val rectangle = new Rectangle(math.abs(x),math.abs(y))
      rectangle.area == x*y
      }
  }

Aquí sucede como en el caso de junit en que estamos filtrando sólo aquellos que nos interesan.

Ahora vamos a ver como se construye y se usa un generador personalizado:

object RectangleGenerator {
  // generator for the Rectangle case class
  val arbRectangleGen:Gen[Rectangle] = for {
    height     width
    rect.area == rect.getHeight * rect.getWidth
  }

  property("hypotenouse of square + int == sum hypotenouses ") = forAll {
    (rect:Rectangle, side:Int) => {
      val square = new Rectangle(side, side).hypotenuse()
      val previousHypotenouse =  rect.hypotenuse()
      rect.add(side)
      square + previousHypotenouse  == rect.hypotenuse()
    }
  }

Como podéis ver el código es realmente sencillo de entender.

Conclusiones & winner

Sobre cual es el mejor, bueno está claro que cada uno tiene sus cosas buenas y sus cosas malas así que daré mis recomendaciones para cada caso:

  • junit-quickcheck: Si lo que se quiere hacer cuadra muy bien con algunos de los parámetros indicados puede ser muy útil, pero además el código puede quedar muy límpio.

  • java quickcheck: Si se necesita un generador determinista o algún tipo de generador más complejo, esta es vuestra solución.

  • ScalaCheck: Si se tienen que combinar varias propiedades o se quiere testear todo un sistema basándose en el paradigma QuickCheck seguramente es la solución más límpia y potente pero con el aliciente de que se debe escribir en Scala.

Mi conclusión es que estas herramientas están bien conocerlas, que pueden ser muy útiles aunque hay que tener cuidado pues pueden añadir una complejidad extra y pueden ocultar que test se están realizando, aunque también es cierto es que usadas sabiamente ahorran mucho código y errores. Eso si, para funcionalidades que tienen que ser probadas exhaustivamente, como interfaces públicas, creo que pueden ser la manera ideal.

Como siempre podéis echar un ojo a nuestros ejemplos en github.