MagMax Blog

Aviso: En este blog puede encontrar código!

Python: Cómo Hacer Pruebas 5: Freshen

| Comments

Después del éxito cosechado con el artículo Python: Cómo hacer pruebas, no puedo evitar aprovechar el tirón y subir el nivel: BDD, o, lo que es lo mismo, Business Driven Development.

Éste va a ser un tema muy arriesgado, ya que en mi vida he conseguido hacer BDD para… el ejemplo que os voy a mostrar. No lo he usado nunca más, aunque me parece bastante interesante el tema.

Aún me guardo un temita en el tintero para otro artículo. Lo dejaré como una sorpresa :D

Qué vamos a utilizar

Hay un montón de formas de hacer BDD. En Python también. Sin embargo, parece que Cucumber se ha transformado en un estándar, aunque es Ruby y no Python.

He estado buscando mucho, evaluando posibilidades como PyHistorian, PyCukes, Lettuce, … Pero ninguno es paquete Debian. Eso no es un problema, claro, porque puede usarse PyPi o empaquetarse a mano, pero sabía yo que podía encontrar algo mejor.

Finalmente, lo he encontrado y… es paquete Debian. Se trata de python-freshen. ¿Y cuál es la mejor característica de esta pequeña maravilla? ¡Pues que se utiliza como un plug-in para nuestro amito nose, al cuál dediqué un artículo!

Así que es requisito indispensable tener instalado python-freshen, el cual ya tiene como dependencia a python-nose, por lo que nos vale.

El problema

Como sabéis, explico mediante ejemplos. En esta ocasión no va a ser menos. El ejemplo que voy a ejecutar es el conocidísimo problema de fizzbuzz. Resumiendo: imprimir los valores hasta uno dado, sustituyendo los múltiplos de 3 por “fizz”, los de 5 por “buzz” y los de 3 y 5 por “fizzbuzz”.

Estructura de ficheros

Lo primero y más importante es ver cómo deben quedar los ficheros que vamos a escribir más adelante. Tendremos 3 ficheros, repartidos en dos directorios. Podéis situar el directorio raíz donde queráis:

.
├── features
│   ├── fizzbuzz.feature
│   └── steps.py
└── fizzbuzz.py

Para ejecutar las pruebas, nos situaremos en el directorio raíz (donde se encuentra “fizzbuzz.py”) y ejecutaremos nose, con la opción —with-freshen:

$ nosetests --with-freshen .

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Podríamos usar también la opción -v para que nos cuente qué test está ejecutando. No he conseguido que sea todo lo bueno que me gustaría, pero… me vale.

Y ahora es cuando creamos los archivos.

Archivo features/fizzbuzz.feature

Este archivo va a especificar nuestro problema, desde un punto de vista funcional y en lenguaje casi natural. Si os lo curráis un poco podéis especificarlo en vuestro idioma, pero… lo siento; en castellano me suena raro y complicaría la explicación, así que lo pongo en inglés:

Feature:
  In order to check my fizzbuzz implementation
  I want to recover the list of fizzbuzz numbers up to 15

  Scenario Outline: Get the list of first values
    Given a <number>
    When you calculate the result
    Then it returns a vector with the <result>

  Examples:
    | number | result                                                                                   |
    | 1      | [1]                                                                                      |
    | 2      | [1, 2]                                                                                   |
    | 3      | [1, 2, 'fizz']                                                                           |
    | 4      | [1, 2, 'fizz', 4]                                                                        |
    | 5      | [1, 2, 'fizz', 4, 'buzz']                                                                |
    | 6      | [1, 2, 'fizz', 4, 'buzz', 'fizz']                                                        |
    | 7      | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7]                                                     |
    | 8      | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8]                                                  |
    | 9      | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz']                                          |
    | 10     | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz']                                  |
    | 11     | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11]                              |
    | 12     | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz']                      |
    | 13     | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13]                  |
    | 14     | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14]              |
    | 15     | [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz']  |

Realmente este archivo se creará poco a poco, añadiendo un “Ejemplo” cada vez.

Lo más importante de aquí son los comienzos de las líneas: las palabras “Feature”, “In order to”, “I want”, “Scenario Outline”, “Examples” y, sobre todo, “Given”, “When”, “Then”.

Hay libros escritos sobre este pseudo lenguaje, así que os dejo que echéis un poco de imaginación o que busquéis en la documentación. Siento no poder explicarlo aquí.

Como se puede observar, a parte de tener un poco cuidado con las palabras con las que empezamos cada línea, es casi lenguaje natural. Según el caso, se puede conseguir un lenguaje natural completo.

En teoría, este archivo podemos enseñárselo al cliente e, incluso, pedirle que nos ayude a construirlo. Con el tiempo, podría llegar a crearlo el propio cliente.

features/steps.py

En este archivo vamos a utilizar expresiones regulares para emparejar las definiciones del archivo features con nuestro programa principal.

Veamos qué aspecto queda:

from freshen import *
from freshen.checks import *

from fizzbuzz import *

@Given('a (\d+)')
def create_data(number):
    scc.number = int(number)

@When('you calculate the result')
def calculate():
    scc.result = fizzbuzz(scc.number)

@Then('it returns a vector with the (.+)')
def check_result(pattern):
    assert_equal(pattern, str(scc.result))

Como puede observarse, realizamos operaciones para Given, When y Then, pasando a los decoradores un parámetro que es la expresión regular que debe cumplirse para ejecutar el propio test.

En nuestro caso, he utilizado una variable de freshen para almacenar mis datos. Existen 3 variables posibles:

  • glc o Global Context, que se mantendrá durante toda la ejecución actual.
  • ftc o Feature Context, que se mantendrá durante esta característica.
  • scc o Scenario Context, que se mantendrá durante este escenario.

En las funciones lo que hago es almacenar el valor inicial, calcular el resultado y comprobar que el resultado es lo esperado, respectivamente.

Se puede observar cómo los matchings de las expresiones regulares se transforman en parámetros de nuestras funciones.

fizzbuzz.py

Finalmente, el archivo solución del problema que, en este caso, es lo que menos nos interesa, pero lo incluyo para que podáis probarlo todo junto:

def fizzbuzz(number):
    return [offuscate(x+1) for x in xrange(number)]

def offuscate(n):
    if n % 3 == 0 and n % 5 == 0:
        return 'fizzbuzz'
    if n % 3 == 0:
        return 'fizz'
    if n % 5 == 0:
        return 'buzz'
    return n

Ejecutando ahora…

Y cuando ahora ejecutamos, el resultado es más que satisfactorio:

$ nosetests --with-freshen .
...............
----------------------------------------------------------------------
Ran 15 tests in 0.031s

OK

Insisto en que con la opción -v se nos mostrará el nombre del escenario actual

Más información

Pues de python-freshen hay poco… algún ejemplo y poco más. Podéis mirarlo en su propia web.

Pero claro, podéis ver más cosas en la web de cucumber o buscar, directamente, RSpec

También podéis echar un ojo a alguno de mis antiguos posts relacionados con las pruebas:

Comments