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.

Anuncios

Clojure Spec

Primero que nada, ¿Qué es clojure spec? Es una biblioteca de clojure que especifica la estructura de los datos, permite la validación de los mismos y puede generar datos a partir del spec.

clojure.spec (o cljs.spec) esta incluida en el core de clojure a partir de la versión 1.9 (aún en estado alpha), así que no es necesario incluir alguna otra biblioteca.

clojure.spec va a permitir que definamos predicados sobre los datos que estamos modelando, permitiendo que estos sean validados en tiempo de ejecución de acuerdo a los predicados definidos. Esto es mucho más poderoso que tener definiciones basadas en tipos.

clojure.spec además permite tener pruebas generativas para probar que las funciones y los datos que definamos funciones de acuerdo a la abstracción de características (predicados) que tenga la información. Se podría decir que clojure.spec tiene los siguientes objetivos de acuerdo a su documentación:

  • Comunicación
  • Especificación unificada en varios contextos
  • Maximizar el apalancamiento a partir del esfuerzo de la especificación
  • Minimizar la intrusión
  • Establecer e iniciar un diálogo sobre el cambio de semánticas y compatibilidad.

Lo anterior debería de automatizar lo siguiente:

  • Validación
  • Reporte de errores
  • Destructuring
  • Instrumentación
  • Generación de datos de prueba
  • Generación de pruebas generativas

Todas estas características permiten el desarrollo de software con un alto nivel de certidumbre, ya que evita la necesidad de crear pruebas unitarias (siempre es difícil establecer cuales son los casos interesantes a probar) y permite tener pruebas generativas con casos de prueba generados a partir de la abstracción de características de los datos modelados.

Notas de braveclojure

Bueno, creo que es hora de ir tomando nota de este pequeño tutorial que es presentado en la página Brave Clojure.

Me salte los capítulos 1 y 2, por lo que me fuí directamente con el tercero.

Using emacs with clojure.

Este capítulo se basa principalmente en mostrar la forma de conectarse a al repl de clojure vía cider-nrepl, así como una introducción a las herramientas que se pueden usar en emacs y sus atajos de teclado.

El capítulo está dividido en las siguientes subsecciones:

  • How to start a REPL process that’s connected to Emacs
  • How to work with Emacs windows
  • A cornucopia of useful key bindings
    • How to evaluate an expression
    • How to compile the file you’re editing
    • How to switch to the namespace of your current file
  • How to handle errors
  • Intro to Paredit

De las cosas más importantes de este capítulo se encuentran los atajos de teclado que utiliza, los que voy a separar de la siguiente forma:

How to start a REPL process that’s connected to Emacs

Atajo Descripción
M-x cider-jack-in Inicia un nuevo nrepl asociado a un archivo .clj
M-x nrepl-close Cierra el nrepl

How to work with Emacs windows

Atajo Descripción
C-x o Mueve el cursor al siguiente frame
C-x 1 Elimina los otros buffers excepto el actual
C-x 2 Divide el buffer en dos. Uno arriba y otro abajo
C-x 3 Divide el buffer en dos. Uno a la izquierda y otro a la derecha
C-x 0 Elimina el buffer actual
C-x b Intercambia entre buffers

A cornucopia of useful key bindings

Atajo Descripción
C-e Se mueve al final de la linea
C-x C-e Evalúa la expresión de Clojure
C-c M-n Establece el namespace actual al REPL
C-c C-k Compila el archivo actual
C-↑, C-↓ Historial del Cider
q Cierra el stack-trace de REPL en caso de error
M-x paredit-mode Establece el modo paredit
M-( Inserta un nuevo par de paréntesis
C-→ Mueve el parentesis al elemento siguiente
C-← Mueve el parentésis al elemento previo

Continuará…