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

Prettify-symbols-mode

Desde la versión 24.4 de emacs se incluye un nuevo modo llamado prettify-symbols-mode, el cuál esta planeado para remplazar la representación de varios identificadores/símbolos usados en los lenguajes de programación por caracteres más estéticos.

Por ejemplo, en el modo lisp (emacs-lisp también), al escribir la palabra reservada lambda, visualmente se transforma en el símbolo λ, pero que en el texto se sigue conservando la palabra escrita.

Esto permite que escribir en diversos lenguajes tenga una notación un poco más “matemática” y visualmente más estética respecto a escribirlo en otros editores.

Para activar esta características, por ejemplo, en el modo lisp hay que sobreescribir la variable lisp--prettify-symbols-alist de la siguiente forma:

(defconst lisp--prettify-symbols-alist
  '(("lambda"  . ?λ)))

Esto significa que la palabra lambda va a ser remplazada por el simbolo λ. Pero también se puede agregar más remplazos para otros modos:

(add-hook clojure-mode-hook
            (lambda ()
              (push '(">=" . ?≥) prettify-symbols-alist)))

En caso de que queramos activar por defecto prettify-symbols en la mayoría de los modos por defecto de emacs, agregamos la siguiente linea a nuestra configuración de emacs:

(global-prettify-symbols-mode +1)

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.

Conceptos de código limpio

Después de observar los cambios que se han ido realizando en los proyectos de código de mis alumnos, me puse a pensar en el concepto de código limpio el cuál para mi significa lo siguiente:

El código limpio es aquel código que es fácil de leer, fácil de entender y muestra un rendimiento óptimo.

¿Pero qué piensan otros programadores acerca del código limpio? para esto, voy a tomar algunas citas del libro Código limpio de Robert C. Martin.

Empecemos con Bjarne Stroustop, desarrollador del lenguaje de programación C++ y autor de The C++ Programming Language

Me gusta que mi código sea elegante y eficaz. La lógica debe ser directa para evitar errores ocultos, las dependencias deben de ser mínimas para facilitar el mantenimiento, el procesamiento de errores completo y sujeto a una estrategia articulada, y el rendimienot debe ser óptimo para que los usuarios no tiendan a estropear el código con optimizaciones sin sentido. El código limpio hace bien una cosa.

Grady Booch, autor de Object Oriented Analysis and Design with Applications

El código limpio es simple y directo. El código limpio se lee como un texto bien escrito. El código limpio no oculta la intención del diseñador si no que muestra nítidas abstracciones y líneas directas de control.

Big’ Dave Thomas, fundador de OTI, el padrino de la estrategia Eclipse.

El código limpio se puede leer y mejorar por parte de un programador que no sea su autor original. Tiene pruebas de unidad y aceptación. Tiene nombres con sentido. Ofrece una y no varias formas de hacer algo. Sus dependencias son mínimas, se definen de forma explícita y ofrece una API clara y mínima. El código debe ser culto en función del lenguaje, ya que no toda la información necesaria se puede expresar de forma clara en el código.

Michael Feathers, autor de Working Effectively with Legacy Code.

Podría enumerar todas las cualidades del código limpio pero hay una principal que engloba a todas ellas. El código limpio siempre parece que ha sido escrito por alguien a quien le importa. No hay nada evidente que hacer para mejorarlo. El autor del código pensó en todos los aspectos posibles y si intentamos imaginar alguna mejora, volvemos al punto de partida y sólo nos queda disfrutar del código que alguien a quien le importa realmente nos ha proporcionado.

Después de observar estas citas y algunas más, creo que las características que debe cumplir el código limpio son las siugientes:

  • Legible
  • Simple
  • Elegancia
  • Eficaz
  • Óptimo
  • Con pruebas

Seguro se pueden agregar más características para un código limpio, un buen código.

Resumen de la Semana [23-29 marzo 2015]

Atajos

Bash

  • Trim leading and trailing whitespace:
sed -i 's/$^{[ \t]*//;s/[ \t]*$//' somefile]]}$

Latex

  • Typesetting temperature:
Water freezes at $32\,^{\circ}\mathrm{F}$.

Un modelo de ramificación exitoso en git.

En este post presento el modelo de desarrollo que he ido introduciendo para todos mis proyectos (de trabajo y privados) desde hace un año, y el cual se ha mostrado realmente exitoso. He estado queriendo escribir sobre él desde hace un tiempo, pero nunca he encontrado el tiempo para hacerlo bien, hasta ahora. No voy a hablar de los detalles en los proyectos, si no de la estrategia de ramificación y gestión de lanzamientos.

git-model.png

Está enfocado alrededor de git como herramienta de versionado de todo nuestro código.

¿Por qué GIT?

Para una discusión detallada acerca de los pros y contras de Git en comparación con otras herramientas de control de versiones, pueden consultar la web. Hay toda una gran discusión sobre eso. Como desarrollador, prefiero Git sobre todas las otras herramientas que hay hoy en día. Git realmente ha cambiado la forma en que los desarrolladores piensan el mezclado y la ramificación de código. Desde el clásico CVS/Subversión del que vengo, siempre se han considerado de miedo (“¡Cuidado con los conflictos de mezclas, ellos pueden morderte!”), algo que pasa de vez en cuando.

Pero con Git, esas acciones son extremadamente baratas y simples, las cuales son consideradas como parte del flujo de trabajo diario. Por ejemplo, en los libros de CVS/Subversión, la ramificación y el mezclado son discutidos en los capítulos finales (para usuarios avanzados), mientras que en cualquier libro de Git, esto es cubierto en el capítulo 3 (cosas básicas).

Como consecuencia de su simplicidad y naturaleza repetitiva, la ramificación y el mezclado ya no son cosas de las que hay que temer. Las herramientas de versionados se suponen que deben ayudar en la ramificación/mezclado sobre cualquier otra cosa.

Suficiente sobre las herramientas, vamos hacia el modelo de desarrollo. El modelo que voy a presentar aquí, es esencialmente un conjunto de procedimientos que todo miembro del equipo tiene que seguir en orden para tener un proceso de software administrado.

Descentralizado pero centralizado.

La configuración del repositorio que usamos y que trabaja bien con este modelo de ramificaciones, es ese con un repositorio central “verdadero”. Note que este repositorio es sólo considerado como el central (ya que Git es un DVCS, no hay tal cosa como un repositorio central a nivel técnico). Nos vamos a referir a este repositorio como origin, ya que este nombre es familiar para todos los usuario de Git.

centr-decent.png

Cada desarrollar pulls y pushes a origin. Pero además de las relaciones push-pull centralizados, cada desarrollador puede también extraer los cambios de otro desarrollador. Por ejemplo, esto podría ser útil para que trabajen dos o más desarrolladores en una gran novedad antes de empujar el trabajo en curso con origin de manera prematura. En la figura anterior, hay equipos secundarios de Alice y Bob, Alice y David, y Clair y David.

Técnicamente, esto significa nada más que Alice ha definido un control, tomado desde el repositorio de bob, y viceversa.

Las ramas principales

En el núcleo, el modelo de desarrollo está inspirado por los modelos existentes que hay. El repositorio central mantiene dos ramas principales con vida infinita:

  • Master
  • Develop

La rama master en origin debería ser familiar para todo usuario de Git. Paralelo a master, existe otra rama llamada develop.

Nosotros consideramos a origin/master para ser la rama principal donde el código fuente de HEAD siempre apunta un estado listo-para-producción.

Nosotros consideramos a origin/develop para ser la rama principal donde el código fuente de HEAD siempre refleja el estado con los últimos cambios liberados para la siguiente liberación. Algunos podrían llamarla como la rama de “integración”. Aquí es donde cualquier nightly build puede ser construida.

Cuando el código fuente en la rama develop alcanza un punto estable y está lista para ser liberada, todos los cambios deberían ser mezclados en master y entonces etiquetados con un número de versión. Más adelante vamos a discutir el cómo se realiza esto.

Por lo tanto, cada vez cuando los cambios son mezclados en master, esto implica que esta en producción por definición. Podemos ser muy estrictos con esto, por lo que, en teoría podemos tener un script de Git para construir nuestro proyecto de manera automática y poner en marcha nuestro software para nuestros servidores de producción cada vez que haya un commit en master.

Soportando ramificaciones

Además de las ramas principales master y develop, nuestro modelo de desarrollo usa una variedad de ramas de soporte para ayudar el desarrollo en paralelo entre los miembros del equipo, así como un fácil seguimiento de características, preparación para las liberaciones en producción y asistir rápidamente a resolver problemas con el sistema en producción. A diferencia de las ramas principales, esas ramas siempre tiene un tiempo de vida limitado, por lo que tendrán que ser eliminadas eventualmente.

Los diferentes tipos de ramas que tal vez usemos son:

  • Ramas de características
  • Ramas de liberación
  • Ramas hotfix

Cada una de esas ramas tiene un propósito específico y están acotados por reglas estrictas, por ejemplo, de que ramas pueden ser originadas y cuales son las ramas con las que deben ser mezcladas. Vamos a revisarlas en un minuto.

De ninguna forma estas son ramas especiales desde una perspectiva técnica. Los tipos de ramas va estar categorizadas por la forma en que vamos a usarlas. Son por supuesto, las viejas y simples ramas de Git.

Ramas de características

Pueden ramificar desde:

  • develop

Deben ser mezcaldas en:

  • Develop

Convención de nombres:

  • Cualquiera, excepto: master, develop, release-* o hotfix-*

Las ramas de características (o algunas veces llamadas ramas puntuales) son usadas para desarrollar nuevas características a futuro o para una versión futura distante. Al iniciar el desarrollo de una característica, el release objetivo en la cuál esta característica va a ser incluida tal vez puede ser desconocida en ese punto. La esencia de una rama de característica es que existe siempre y cuando la función se encuentra en desarrollo, pero con el tiempo se mezcla en develop (añadida como una característica para la próxima liberación) o deshechada (en caso de ser un experimente descepcionante).

Las ramas de características normalmente viven en los repositorios de los desarrollador, no en origin.

fb.png

Creando una rama de características

Cuando iniciamos el trabajo en nueva característica, debemos ramificar desde la rama develop.

$ git checkout -b miCaracteristica develop

Incorporando una característica terminado en develop

Las características terminadas pueden ser mezcladas en la rama develop cuando van a ser añadidas de manera definitiva para la próxima liberación:

$ git checkout Develop
# Intercambio a la rama 'develop'
$ git merge --no-ff miCaracteristica
# Updating ea1b82a..05e9557
#(Summary of changes)
$ git branch -d myfeature
# Deleted branch myfeature (was 05e9557).
$ git push origin develop

El flag –no-ff causa que la mezcla siempre cree un nuevo objeto commit, incluso si la mezcla podría ser realizada con un fast-foward. Esto evita la perdida de información sobre la existencia histórica de una rama de características y agrupar todas las confirmaciones que agregan la función. Comparemos:

merge-without-ff.png

En el último caso, es imposible ver desde la historia de Git cuales fueron los commits que se hicieron para implementar una característica. Podrías tener que leer de manera manual todos los logs. Revertir toda una característica (conjunto de commits), es un dolor de cabeza en la última situación, mientras que es fácilmente hacerlo si hubieras usado el flag –no-ff.

Sí, vas a crear unos cuantos más commits (vacíos), pero la ganancia es mucho más grande que ese costo.

Por desgracia, no he encontrado una manera de hacer –no-ff el comportamiento predeterminado de git merge, pero realmente es lo que debería ser.

Ramas de lanzamiento

Pueden ser ramificadas desde:

  • develop

Deben ser mezcladas en:

  • develop y master

Convención de nombres:

  • release-*

Las ramas de lanzamiento soportan la preparación de una nueva liberación en producción. Ellas permiten realizar cambios de último minuto. Así también realizar correcciones y preparar los metadatos para el lanzamiento (números de versión, fechas de construcción, etc). Haciendo todo este trabajo sobre una rama de lanzamiento, la rama de develop está lista para recibir características para el siguiente gran lanzamiento.

El principal momento para ramificar una nueba rama de lanzamiento desde develop es cuando el desarrollo refleja un estado deseado para una nueva liberación. Al menos todas las características son enviadas para el lanzamiento, construidas al ser mezcladas en develop en este punto. Todas las características enviadas a lanzamientos futuros no deben estar incluidas – deben esperar a su momento.

En el momento de iniciar una rama de lanzamiento cuando se le asigna un número de versión (no antes). Hasta ese momento, la rama develop refleja los cambios para la “próxima liberación”, no está claro si ese próximo lanzamiento se convertirá en 0.3 o 1.0, hasta que se inicie la rama de lanzamiento.

Creando una rama de lanzamiento

La ramas de lanzamiento son creadas desde la rama develop. Por ejemplo, digamos que la versión 1.1.5 es la versión actual en producción y nosotros tenemos una gran liberación pronto. El estado de develop está listo para la “siguiente liberación” y nosotros tenemos que decidir que va ir en la versión 1.2 (en lugar de 1.1.6 o 2.0). Así nosotros ramificamos y damos la rama de lanzamiento el nombre que refleje el nuevo número de versión.

git checkout -b release-1.2 develop
# Creamos una nueva rama "release-1.2" y nos movemos a ella
./bump-version.sh 1.2
# Cambiamos los archivos indicado la versión 1.2
git commit -a -m "Cambio de versión a 1.2"
# [release-1.2 74d9424] Cambio de versión a 1.2
# 1 files changed, 1 insertions(+), 1 deletions(-)

Después de crear la nueva rama y movernos a ella, cambiamos el número de versión. Aquí bump-version.sh es un script imaginario que cambia algunos archivos en la copia de trabajo para reflejar la nueva versión (Esto puede ser por supuesto un cambio manual, el punto es cambiar algunos archivos). Entonces, el número de versión es commiteado.

Esta nueva rama puede existir por un tiempo, hasta que la liberación sea terminada definitivamente. Durante ese tiempo, se reparan los errores que puedan existir en esa rama (en lugar de la rama de desarrollo). Añadir nuevas características grandes está estrictamente prohibido. Estas deben ser mezcladas en develop y esperar para el siguiente lanzamiento.

Terminando una rama de lanzamiento

Cuando el estado de una rama de lanzamiento está lista para ser un lanzamiento rela, algunas acciones deben ser realizadas. Primero, la rama de lanzamiento es mezclada en master (Debido a que cada commit en master es un nuevo lanzamiento por definición, recuerdas?). Después, ese commit en master debe ser etiquetado para tener una referencia futura. Finalmente, los cambios realizados en la rama de liberación deben ser mezclados de nuevo en develop.

Los primeros dos pasos en GIT:

git checkout master
# Switched to branch 'master'
git merge --no-ff release-1.2
# Merge made by recursive.
# (Summary of changes)
$ git tag -a 1.2

El lanzamiento está hecho, y etiquetado para futuras referencias.

Para mantener esos cambios realizados en la rama de liberación, debemos de mezclarlos de vuelta en develop. En GIT:

git checkout develop
# Switched to branch 'develop'
git merge --no-ff release-1.2
# Merge made by recursive.
# (Summary of changes)

Este paso podría tener algunos conflictos al mezclar (probablemente algunos, desde que cambios el número de versión). Si es así, los corregimos y hacemos commit.

Ahora debemos eliminar la rama de lanzamiento debido a que ya no la vamos a seguir usando.

git branch -d release-1.2
# Deleted branch release-1.2 (was ff452fe).

Ramas de correcciones

Pueden ser ramificadas desde:

  • Master

Deben ser mezcladas en:

  • develop y master

Convención de nombrado:

  • hotfix-*

Las ramas de correción son muy parecidas a las ramas de lanzamiento en que ellas también se preparan para un nuevo lanzamiento en producción, aunque no sea planeado. Estas ramas nacen de la necesidad de cambiar un estado no deseado de una versión en producción. Cuando hay un error crítico en producción, esto debe ser resuelto inmediatamente, entonces se ramifica una rama de corrección a partir de la correspondiente etiqueta que marca la versión en producción.

Esencialmente el trabajo del equipo (sobre develop) puede continuar, mientras otra persona puede preparar una corrección rápida en producción.

Creando una rama de corrección

hotfix.png

Las ramas de corrección son creadas a partir de la rama master. Por ejemplo, digamos que la versión 1.2 es la versión en producción actual y está causando problemas debido a un error. Pero los cambios en develop aún son inestables. Nosotros podemos ramificar una rama de corrección e iniciar a resolver el problema.

$ git checkout -b hotfix-1.2.1 master
# Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
# Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
# [hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
# 1 files changed, 1 insertions(+), 1 deletions(-)

¡No olvides realizar el cambio de versión después de ramificar!

Entonces, corregimos el error y comiteamos el error en uno o más commits separados.

git commit -m "Fixed severe production problem"
# [hotfix-1.2.1 abbe5d6] Fixed severe production problem
# 5 files changed, 32 insertions(+), 17 deletions(-)

Terminando una rama de corrección

Cuando terminemos, la corrección del error necesita ser mezclada de nuevo en master, pero también necesitamos mezclarla de vuelta en develop, de forma que podamos asegurar que la corrección sea incluida en la siguiente versión también. Esto es completamente similar a como las ramas de lanzamiento son terminadas.

Primero, actualizamos master y la etiqueta de la liberación:

git checkout master
# Switched to branch 'master'
git merge --no-ff hotfix-1.2.1
# Merge made by recursive.
# (Summary of changes)
git tag -a 1.2.1

Lo siguiente es incluir la corrección en develop:

git checkout develop
# Switched to branch 'develop'
git merge --no-ff hotfix-1.2.1
# Merge made by recursive.
# (Summary of changes)

La única excepción a la regla es que, cuando una rama de lanzamiento exista, la corrección necesita ser mezclada en al rama de liberación en lugar de la de develop. Mezclando la corrección en la rama de lanzamiento, eventualmente va a ser mezclada en develop también. (Si el trabajo en develop requiere mezclar esta corrección y no pueden esperar hasta que la rama de lanzamiento termine, puedes mezclar la corrección seguramente en develop también).

Finalmente, eliminamos la rama de corrección:

git branch -d hotfix-1.2.1
# Deleted branch hotfix-1.2.1 (was abbe5d6).

Comentario: Este documento es una traducción de “A succesful Git branching model”.