Pruebas generativas

¿Qué son las pruebas generativas?

Las pruebas generativas son pruebas sobre código que se realizan usando datos «aleatorios» sobre las funciones que nosotros escribimos. La idea es probar estos datos «aleatorios» contra propiedades de la salida deseada de las funciones.

En Clojure existe una biblioteca llamada clojure.test.check, la cuál se encarga de proveer herramientas para construir estas pruebas generativas.

Las pruebas generativas, básicamente se encargan de separar la definición del dominio de entrada respecto al concepto de generadores y validadores.

Pruebas en clojure

Clojure, a través del espacio de nombres clojure.test, provee funciones y macros para realizar pruebas unitarias en sobre las funciones que implementemos en clojure.

Las pruebas en clojure generalmente lucen de la siguiente forma:

(require '[clojure.test :refer [deftest]])
(defn add
  "Realiza la suma de |x| con |y|"
  [x y]
  (+ x y))

(deftest add-x-to-y
  (is (= 5 (add 2 3))))

El ejemplo anterior, es un ejemplo pequeño de como realizar pruebas unitarias en clojure. Pero en ocasiones esto no es suficiente, para esto, se define una nueva forma de probar el código que implementemos utilizando datos generados de forma «aleatoria» para introducirla a nuestras funciones y a partir de las propiedades de la salida, determinar si estas funciones son correctas o no. Este tipo de pruebas se les conoce como pruebas generativas.

El espacio de nombres clojure.test.check (una biblioteca inspirada en Quickcheck de Haskell), provee una serie de funciones y macros para poder crear pruebas generativas sobre el código que nosotros escribimos. Estas pruebas generativas como mencionamos previamente, están sustentadas sobre la prueba de propiedades de las salidas de una función.

Ejemplos de como realizar pruebas generativas usando clojure.test.check las podemos encontrar en la documentación del mismo proyecto, la cuál podemos acceder a través de esta liga.

Aún así se agrega a este documento un ejemplo pequeño como muestra:

(require '[clojure.test.check :as tc]
         '[clojure.test.check.generators :as gen]
         '[clojure.test.check.properties :as prop])

(defn ascending?
  "clojure.core/sorted? doesn't do what we might expect, so we write our own
  function"
  [coll]
  (every? (fn [[a b]] (<= a b))
          (partition 2 1 coll)))

(def property
  (prop/for-all [v (gen/vector gen/int)]
    (let [s (sort v)]
      (and (= (count v) (count s))
           (ascending? s)))))

;; test our property
(tc/quick-check 100 property)

Entendiendo el como usar las pruebas generativas

Del código anterior, se invocan los siguientes espacios de nombres:

(require '[clojure.test.check :as tc]
         '[clojure.test.check.generators :as gen]
         '[clojure.test.check.properties :as prop])

Los cuáles son los encargados de proveer funciones y macros para la generación y prueba de funciones. De estos espacios de nombres podemos observar las siguientes características:

tc
Contiene una única función quick-check que es utilizada probar funciones con valores generados por una función generador.
gen
Contiene funciones para generar datos basados en propiedades.
prop
Provee sugar macros para definir parámetros y funciones para ser probadas por los generadores.

Más info sobre estos espacios de nombres, pueden ser consultados en clojure.test.check

Una vez identificadas los distintos tipos de funciones y macros provistos por clojure.test.check, procedemos a definir una función y una propiedad a probar para esa función.

(defn ascending?
  "clojure.core/sorted? doesn't do what we might expect, so we write our own
  function"
  [coll]
  (every? (fn [[a b]] (<= a b))
          (partition 2 1 coll)))

Lo anterior define una función que verifica que para cada para par dentro de la colección que pasamos como parámetro, esta siempre sea ordenada, i.e. exista un ordenamiento.

Una vez hecho, procedemos a escribir la propiedad:

(def property
  (prop/for-all [v (gen/vector gen/int)]
    (let [s (sort v)]
      (and (= (count v) (count s))
           (ascending? s)))))

La cuál verifica que podamos ordenar una colección y además que dicha colección se encuentra ordenada de forma ascendente. La colección usada es generada por (gen/vector gen/int), la cuál es una combinación de generadores que dice que genere un vector de enteros y lo guarde en una variable v, la cuál después se la pasamos a la forma para evaluar nuestra expresión.

Hecho lo anterior, se utiliza la función tc/quick-check, indicándole el número de muestras a generar, en este caso le decimos que 100 de la siguiente forma:

(tc/quick-check 100 property)

Hasta aquí vamos a detener este artículo introductorio, esperemos que podamos continuar con la generación de bases de datos de batería para probar nuestro código, para ello intentaremos hacer uso de las pruebas generativas de clojure.test.check.