Documentación y Testing ----------------------- "Si lo que dice ahí no está en el manual, está equivocado. Si está en el manual es redundante." -- Califa Omar, Alejandría, Año 634. .. admonition:: FIXME #. Cambiar el orden de las subsecciones (probablemente) #. ¿Poner este capítulo después del de deployment? #. Con el ejemplo nuevo, meter setUp / tearDown ¿Pero cómo sabemos si el programa hace *exactamente* lo que dice el manual? Bueno, pues *para eso* (entre otras cosas) están los tests [#]_. Los tests son la rama militante de la documentación. La parte activa que se encarga de que ese manual no sea letra muerta e ignorada por perder contacto con la realidad, sino un texto que refleja lo que realmente existe. .. [#] También están para la gente mala que no documenta. Si la realidad (el funcionamiento del programa) se aparta del ideal (el manual), es el trabajo del test chiflar y avisar que está pasando. Para que esto sea efectivo tenemos que cumplir varios requisitos: Cobertura Los tests tienen que poder detectar todos los errores, o por lo menos aspirar a eso. Integración Los tests tienen que ser ejecutados ante cada cambio, y las diferencias de resultado explicadas. Ganas El programador y el documentador y el tester (o sea uno) tiene que aceptar que hacer tests es necesario. Si se lo ve como una carga, no vale la pena: vas a aprender a ignorar las fallas, a hacer "pasar" los tests, a no hacer tests de las cosas que sabés que son difíciles. Por suerte en Python hay muchas herramientas que hacen que testear sea, si no divertido, por lo menos tolerable. Docstrings ~~~~~~~~~~ Tomemos un ejemplo semi-zonzo: una función para cortar pedazos de archivos [#]_. .. [#] Ejemplo idea de Facundo Batista. .. admonition:: jack.py ``jack.py`` va a ser un programa que permita cortar pedazos de archivos en dos ejes. Es decir que le podemos indicar: * De la línea A a la línea B * De la columna X a la columna Y Va a recibir esos parámetros, un nombre de archivo, y produce el corte en la salida standard. Comencemos con una función que corta en el eje vertical, cortando por filas: .. admonition:: Generadores Esta función que usa ``yield`` es lo que se llama un `generador. `_ Trabajar de esta manera es más eficiente. Por ejemplo, si ``lineas`` fuera un objeto archivo, esto funciona *sin leer todo el archivo en memoria*. Y si ``lineas`` es una lista... bueno, igual funciona. .. class:: titulo-listado jack1.py .. class:: listado .. code-block:: python :linenos: :include: codigo/4/jack1.py Esa cadena debajo del ``def`` se llama docstring y *siempre* hay que usarla. ¿Por qué? * Es el lugar "oficial" para explicar qué hace cada función * ¡Sirven como ayuda interactiva! .. code-block:: pycon >>> import jack1 >>> help(jack1.selecciona_lineas) Help on function selecciona_lineas in module jack1: selecciona_lineas(lineas, desde=0, hasta=-1) Filtra el texto dejando sólo las lineas [desde:hasta]. A diferencia de los iterables en python, no soporta índices negativos. * Usando una herramienta como `epydoc `_ se pueden usar para generar una guía de referencia de tu módulo (¡manual gratis!) * Son el hogar de los doctests. Doctests ~~~~~~~~ "Los comentarios mienten. El código no." -- Ron Jeffries Un comentario mentiroso es peor que ningún comentario. Y los comentarios se vuelven mentira porque el código cambia y nadie edita los comentarios. Es el problema de repetirse: uno ya dijo lo que quería en el código, y tiene que volver a explicarlo en un comentario; a la larga las copias divergen, y siempre el que está equivocado es el comentario. Un doctest permite **asegurar** que el comentario es cierto, porque el comentario tiene código de su lado, no es sólo palabras. Y acá viene la primera cosa importante de testing: Uno quiere testear **todos** los comportamientos intencionales del código. Si el código se supone que ya hace algo bien, aunque sea algo muy chiquitito, es el momento ideal para empezar a hacer testing. Si vas a esperar a que la función sea "interesante", ya va a ser muy tarde. Vas a tener un déficit de tests, vas a tener que ponerte un día sólo a escribir tests, y vas a decir que testear es aburrido. ¿Cómo sé yo que ``selecciona_lineas`` hace lo que yo quiero? ¡Porque la probé! Como no soy el mago del código que lo escribe y le anda sin errores off-by-one, hice esto en el intérprete interactivo: .. code-block:: pycon >>> from jack1 import selecciona_lineas >>> print range(10)[5:10] [5, 6, 7, 8, 9] >>> print list(selecciona_lineas(range(10), 5, 10)) [5, 6, 7, 8, 9] Y dije, sí, ok, eso es coherente. Si no hubiera hecho ese test manual no tendría la más mínima confianza en este código, y creo que todos hacemos esta clase de cosas, ¿o no?. El problema con este testing manual ad hoc es que lo hacemos una vez, la función hace lo que se supone debe hacer (al menos por el momento), y nos olvidamos. Por suerte *no tiene que ser así*, gracias a los doctests. De hecho, el doctest es poco más que cortar y pegar esos tests informales que mostré arriba. Veamos una versión con algunos doctests, y más funciones. .. raw:: pdf PageBreak .. class:: titulo-listado jack2.py .. class:: listado .. code-block:: python :linenos: :include: codigo/4/jack2.py :end-at: return resultado Eso es todo lo que se necesita para implementar doctests. ¡En serio!. ¿Y cómo hago para saber si los tests pasan o fallan? Hay muchas maneras. Tal vez la que más me gusta es usar `Nose `_, una herramienta cuyo único objetivo es hacer que testear sea más fácil. :: $ nosetests --with-doctest -v jack2.py Doctest: jack2.selecciona_columnas ... ok Doctest: jack2.selecciona_fragmento ... ok Doctest: jack2.selecciona_lineas ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.051s OK Lo que hizo ``nosetests`` es "descubrimiento de tests" (test discovery). Toma la carpeta actual o el archivo que indiquemos (en este caso ``jack2.py``), encuentra las cosas que parecen tests y las usa. El parámetro ``--with-doctest`` es para que reconozca doctests (por default los ignora), y el ``-v`` es para que muestre cada cosa que prueba. De ahora en más, cada vez que el programa se modifique, volvemos a correr los tests. Si falla alguno que antes andaba, es una regresión, paramos de romper y la arreglamos. Si pasa alguno que antes fallaba, es un avance, nos felicitamos y nos damos un caramelo. Pero supongamos que lo que queremos es un nuevo feature. ¿Qué hacemos entonces? ¡Agregamos un test que falla! Bienvenido al mundo del TDD o "Desarrollo impulsado por tests" (Test Driven Development). La idea es que, en general, si sabemos que hay un bug, o falta un feature, seguimos este proceso: * Creamos un test que falla. * Arreglamos el código para que no falle el test. * Verificamos que no rompimos otra cosa usando el test suite. Un test que falla es **bueno** porque nos marca que cosas hay que corregir. Si los tests son piolas, y cada uno prueba una sola cosa [#]_ , entonces hasta nos va a indicar qué parte del código es la que está rota. .. [#] Un test que prueba muchas cosas juntas no es un buen test, porque al fallar no sabés por qué. Eso se llama granularidad de los tests y es muy importante. Un problema de ``jack2.py`` es que no es un script, sino un módulo. Yo quiero que al llamarlo desde la línea de comando haga algo interesante. ¿Cómo lo hago? Bueno, hay muchas maneras, acá les voy a mostrar la más fácil, el módulo `commandline `_ Todo lo que se necesita es crear una función que tome los argumentos que queremos pasar por línea de comandos, y muy poco más: .. class:: titulo-listado jack2.py .. class:: listado .. code-block:: python :linenos: :include: codigo/4/jack2.py :linenos_offset: :start-at: def procesa ¿Qué pasa ahora si usamos jack2 como un script cualquiera? :: $ python2 jack2.py --help Usage: jack2.py archivo [fila1 [fila2 [col1 [col2]]]] [Options] Options: -h, --help show this help message and exit --fila1=FILA1 default=0 --fila2=FILA2 default=9223372036854775807 --col1=COL1 default="none" --col2=COL2 default="none" --archivo=ARCHIVO default="none" Abre un archivo y lo corta según se pida. ¿No está bueno? Muy sencillo, y suficiente para lo que necesitamos. Además, al ser el parseo de la línea de comandos muy obvio y directo, es posible testear el script poniendo tests equivalentes en ``procesa_archivo``. Entonces... ¿Tiene algún bug este programa? ¡Tiene *muchos*! La idea general es una herramienta al estilo unix, como ``tail`` o ``head``, y en ese contexto, fracasa miserablemente:: $ python2 jack2.py /etc/passwd 1 5 $ ¡No devuelve nada! ----------------------------------------- ¿Notaste que agregar tests de esta forma no se siente como una carga? Es parte natural de escribir el código, pienso, "uy, esto no debe andar", meto el test como creo que debería ser en el docstring, y de ahora en más sé si eso anda o no. Por otro lado te da la tranquilidad de "no estoy rompiendo nada". Por lo menos nada que no estuviera funcionando exclusivamente por casualidad. Por ejemplo, ``gaso1.py`` pasaría el test de la palabra "la" y ``gaso2.py`` fallaría, pero no porque ``gaso1.py`` estuviera haciendo algo bien, sino porque respondía de forma afortunada. Cobertura ~~~~~~~~~ Es importante que nuestros tests "cubran" el código. Es decir que cada parte sea usada por lo menos una vez. Si hay un fragmento de código que ningún test utiliza nos faltan tests (o nos sobra código [#]_) .. [#] El código muerto en una aplicación es un problema serio, molesta cuando se intenta depurar porque está metido en el medio de las partes que sí se usan y distrae. La forma de saber qué partes de nuestro código están cubiertas es con una herramienta de cobertura ("coverage tool"). Veamos una en acción:: [ralsina@hp python-no-muerde]$ nosetests --with-coverage --with-doctest \ -v gaso3.py buscaacento1.py Doctest: gaso3.gas ... ok Doctest: gaso3.gasear ... ok Doctest: buscaacento1.busca_acento ... ok Name Stmts Exec Cover Missing ----------------------------------------------- buscaacento1 6 6 100% encodings.ascii 19 0 0% 9-42 gaso3 10 10 100% ----------------------------------------------- TOTAL 35 16 45% ------------------------------------------------------------- Ran 3 tests in 0.018s OK Al usar la opción ``--with-coverage``, nose usa el módulo ``coverage.py`` para ver cuáles líneas de código se usan y cuales no. Lamentablemente el reporte incluye un módulo de sistema, ``encodings.ascii`` lo que hace que los porcentajes no sean correctos. Una manera de tener un reporte más preciso es correr ``coverage report`` luego de correr ``nosetests``:: [ralsina@hp python-no-muerde]$ coverage report Name Stmts Exec Cover ---------------------------------- buscaacento1 6 6 100% gaso3 10 10 100% ---------------------------------- TOTAL 16 16 100% Ignorando ``encodings.ascii`` (que no es nuestro), tenemos 100% de cobertura: ese es el ideal. Cuando ese porcentaje baje, deberíamos tratar de ver qué parte del código nos estamos olvidando de testear, aunque es casi imposible tener 100% de cobertura en un programa no demasiado sencillo. Coverage también puede crear reportes HTML mostrando cuales líneas se usan y cuales no, para ayudar a diseñar tests que las ejerciten. .. note:: FIXME Mostrar captura salida HTML** Límites de los doctests ~~~~~~~~~~~~~~~~~~~~~~~ ¿Entonces hacemos doctests y ya está? No. Los doctests son completamente inútiles en ciertos casos. Por ejemplo: es posible tener un módulo que necesite 200 o 300 tests. ¿Vamos a meter todo eso en los docstrings? ¿Y vamos a tener docstrings de 1000 líneas llenas de código? Eso ni siquiera cumple el objetivo de "dar algunos ejemplos". Tener 1000 ejemplos es a veces peor que no tener ninguno. Así que no, no alcanza con doctests. Para hacer testing en serio necesitás hacer *test suites*. Son herramientas complementarias. Los doctests son básicamente documentación para que los demás sepan cómo se usa. Su componente "test" es principalmente para que la documentación sea precisa. Pero por su misma naturaleza, los doctests no pueden ser exhaustivos, excepto para funciones triviales. Por suerte, hay una herramienta razonable para eso en la biblioteca standard, `el módulo unittest. `_ Sin embargo, no vamos a usar eso, si no, nuevamente, nose. ¿Por qué? Porque es menos burocrático. Para hacer un test con unittest, tenés que: * Crear una clase que herede ``unittest.TestCase``. * Definir dentro de esa clase una función ``test_algo``. Con nose podés hacer exactamente lo mismo. O crear una función. O una clase con tests adentro que no herede ``TestCase``. Y además soporta correr los doctests también. No es una diferencia enorme, pero es algo menos de laburo, y ``-laburo == bueno``. Lo anterior, hecho distinto ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. class:: titulo-listado gaso4.py .. class:: listado .. code-block:: python :linenos: :include: codigo/4/gaso4.py :start-at: # Test Suite Vemos cómo usamos nosetests con este nuevo test suite: :: $ nosetests codigo/4/gaso4.py -v Test de palabra aguda. ... ok Test de palabra grave. ... ok Test palabra con acento ortográfico. ... ok Test palabra grave con acento prosódico. ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.012s OK Algunos detalles a favor de este approach: * Podemos ponerles descripciones a los tests. * Tenemos más libertad de hacer cosas antes y después de la llamada a la función que testeamos. * Es más natural y flexible la manera de hacer los ``asserts`` en cada test. Pero testing no termina ahí. Estos son tests obvios de funciones *muy* fáciles de testear, toman u parámetro, dan un resultado, no requieren nada, no tienen efectos secundarios, son una bici con rueditas. Vamos a pasar ahora a un ejemplo bastante más "real". Y las cosas se van a volver ligeramente más densas. Mocking ~~~~~~~ La única manera de reconocer al maestró del disfraz es su risa. Se ríe "jo jo jo". -- Inspector Austin, Backyardigans A veces para probar algo, se necesita un objeto, y no es práctico usar el objeto real por diversos motivos, entre otros: * Puede ser un objeto "caro": una base de datos. * Puede ser un objeto "inestable": un sensor de temperatura. * Puede ser un objeto "malo": por ejemplo un componente que aún no está implementado. * Puede ser un objeto "no disponible": una página web, un recurso de red. * Simplemente quiero "separar" los tests, quiero que los errores de un componente no se propaguen a otro. [#]_ .. [#] Esta separación de los elementos funcionales es lo que hace que esto sea "unit testing": probamos cada unidad funcional del código. * Estamos haciendo doctests de un método de una clase: la clase no está instanciada al ejecutar el doctest. Para resolver este problema se usa mocking. ¿Qué es eso? Es una manera de crear objetos falsos que hacen lo que uno quiere y podemos usar en lugar del real. Una herramienta sencilla de mocking para usar en doctests es `minimock `_. Apartándonos de nuestro ejemplo por un momento, ya que no se presta a usar mocking sin inventar nada ridículo, pero aún así sabiendo que estamos persiguiendo hormigas con aplanadoras... .. class:: titulo-listado mock1.py .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/4/mock1.py :start-at: def largo_de_pagina Es especialmente interesante esta parte: .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/4/mock1.py :start-at: >>> largo_de_pagina :end-before: ''' ¿Qué es exactamente lo que estamos comprobando en ese doctest? * Que se llamó exactamente a esas funciones y a ninguna otra. * Que se las llamó con los argumentos correctos. * Que cuando nuestra función recibió los datos de esta "internet falsa", hizo el cálculo correcto. Por supuesto es posible hacer algo muy similar en forma de test, en vez de doctest, usando otra herramienta de mocking, `Mock `_: .. class:: titulo-listado mock2.py .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/4/mock2.py :start-at: from mock import Ojo que este último ejemplo de mock no hace exactamente lo mismo que el primero. Por ejemplo, no se asegura que no llamé o usé otros atributos de los objetos ``Mock``... Hay otras variantes de mocks, por ejemplo, los mocks "record and replay" (que no me gustan mucho, porque producen tests muy opacos, y te tientan a tocar acá y allá hasta que el test pase en vez de hacer un test útil). La Máquina Mágica ~~~~~~~~~~~~~~~~~ Mucho se puede aprender por la repetición bajo diferentes condiciones, aún si no se logra el resultado deseado. -- Archer J. P. Martin Un síntoma de falta de testing es la máquina mágica. Es un equipo en particular en el que el programa funciona perfectamente. A nadie más le funciona, y el desarrollador nunca puede reproducir los errores de los usuarios. ¿Por qué sucede esto? Porque si no funcionara en la máquina del desarrollador, él se habría dado cuenta. Por ese motivo, los desarrolladores siempre tenemos exactamente la combinación misteriosa de versiones, carpetas, software, permisos, etc. que resuelve todo. Para evitar estas suposiciones implícitas en el código, lo mejor es tener un entorno **repetible** en el que correr los tests. O mejor aún: muchos. De esa forma uno sabe "este bug no se produce si tengo la versión X del paquete Y con python 2.6" y puede hacer el diagnóstico hasta encontrar el problema de fondo. Por ejemplo, para un programa mío llamado rst2pdf [#]_, que requiere un software llamado ReportLab, y (opcionalmente) otro llamado Wordaxe, los tests se ejecutan en las siguientes condiciones: .. [#] Si estás leyendo este libro en PDF o impreso, probablemente estás viendo el resultado de rst2pdf. * Python 2.4 + Reportlab 2.4 * Python 2.5 + Reportlab 2.4 * Python 2.6 + Reportlab 2.4 * Python 2.6 + Reportlab 2.3 * Python 2.6 + Reportlab 2.4 + Wordaxe Hasta que no estoy contento con el resultado de *todas* esas corridas de prueba, no voy a hacer un release. De hecho, si no lo probé con todos esos entornos no estoy contento con un *commit*. ¿Cómo se hace para mantener todos esos entornos de prueba en funcionamiento? Usando `virtualenv `_. Virtualenv no se va a encargar de que puedas usar diferentes versiones de Python [#]_, pero sí de que sepas exactamente qué versiones de todos los módulos y paquetes estás usando. .. [#] Eso es cuestión de instalar varios Python en paralelo, y depende (entre otras cosas) de qué sistema operativo estés usando. Una herramienta interesante es `tox `__ Tomemos como ejemplo la versión final de la aplicación de reducción de URLs del capítulo La vida es corta. Esa aplicación tiene montones de dependencias que no hice ningún intento de documentar o siquiera averiguar mientras la estaba desarrollando. Veamos como ``virtualenv`` nos ayuda con esto. Empezamos creando un entorno virtual vacío: :: [python-no-muerde]$ cd codigo/2/ [2]$ virtualenv virt --no-site-packages --distribute New python executable in virt/bin/python Installing distribute...................................done. La opción ``--no-site-packages`` hace que nada de lo que instalé en el Python "de sistema" afecte al entorno virtual. Lo único disponible es la biblioteca standard. La opción ``--distribute`` hace que utilice Distribute en lugar de setuptools. No importa demasiado por ahora, pero para más detalles podés leer el capítulo de deployment. :: [2]$ . virt/bin/activate (virt)[2]$ which python /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/2/virt/bin/python ¡Fijáte que ahora python es un ejecutable dentro del entorno virtual! Eso es activarlo. Todo lo que haga ahora funciona con **ese** entorno, si instalo un programa con ``pip`` se instala ahí adentro, etc. El ``(virt)`` en el prompt indica cuál es el entorno virtual activado. Probemos nuestro programa:: (virt)[2]$ python pyurl3.py Traceback (most recent call last): File "pyurl3.py", line 14, in from twill.commands import go, code, find, notfind, title ImportError: No module named twill.commands Bueno, necesitamos ``twill``:: (virt)[2]$ pip install twill Downloading/unpacking twill Downloading twill-0.9.tar.gz (242Kb): 242Kb downloaded Running setup.py egg_info for package twill Installing collected packages: twill Running setup.py install for twill changing mode of build/scripts-2.6/twill-fork from 644 to 755 changing mode of /home/ralsina/Desktop/proyectos/ python-no-muerde/codigo/4/virt/bin/twill-fork to 755 Installing twill-sh script to /home/ralsina/Desktop/proyectos/ python-no-muerde/codigo/4/virt/bin Successfully installed twill Si sigo intentando ejecutar ``pyurl3.py`` me dice que necesito ``storm.locals`` (instalo storm), ``beaker.middleware`` (instalo beaker), ``authkit.authenticate`` (instalo authkit). Como ``authkit`` también trata de instalar ``beaker`` resulta que las únicas dependencias reales son twill, storm y authkit, lo demás son dependencias de dependencias. Con esta información tendríamos suficiente para crear un script de instalación, como veremos en el capítulo sobre deployment. De todas formas lo importante ahora es que tenemos una base estable sobre la cual diagnosticar problemas con el programa. Si alguien nos reporta un bug, solo necesitamos ver qué versiones tiene de: * Python: porque tal vez usamos algo que no funciona en su versión, o porque la biblioteca standard cambió. * Los paquetes que instalamos en ``virtualenv``. Podemos ver cuales son fácilmente:: (virt)[2]$ pip freeze AuthKit==0.4.5 Beaker==1.5.3 Paste==1.7.3.1 PasteDeploy==1.3.3 PasteScript==1.7.3 WebOb==0.9.8 decorator==3.1.2 distribute==0.6.10 elementtree==1.2.7-20070827-preview nose==0.11.3 python-openid==2.2.4 storm==0.16.0 twill==0.9 wsgiref==0.1.2 De hecho, es posible usar la salida de ``pip freeze`` como un archivo de requerimientos, para reproducir *exactamente* este entorno. Si tenemos esa lista de requerimientos en un archivo ``req.txt``, entonces podemos comenzar con un entorno virtual vacío y "llenarlo" exactamente con eso en un solo paso:: [2]$ virtualenv virt2 --no-site-packages --distribute New python executable in virt2/bin/python Installing distribute..............................done. [2]$ . virt2/bin/activate (virt2)[2]$ pip install -r req.txt Downloading/unpacking Beaker==1.5.3 (from -r req.txt (line 2)) Real name of requirement Beaker is Beaker Downloading Beaker-1.5.3.tar.gz (46Kb): 46Kb downloaded : : : : Successfully installed AuthKit Beaker decorator elementtree nose Paste PasteDeploy PasteScript python-openid storm twill WebOb Fijáte como pasamos de "no tengo idea de qué se necesita para que esta aplicación funcione" a "con este comando tenés exactamente el mismo entorno que yo para correr la aplicación". Y de la misma forma, si alguien te dice "no me autentica por OpenID" podés decirle: "dame las versiones que tenés instaladas de AuthKit, Beaker, python-openid, etc.", hacés un ``req.txt`` con las versiones del usuario, y podés reproducir el problema. ¡Tu máquina ya no es mágica! De ahora en más, si te interesa la compatibilidad con distintas versiones de otros módulos, podés tener una serie de entornos virtuales y testear contra cada uno. Sacando tu programa a pasear: Tox ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are many factors in the environment that are “problems” that require “solutions”. -- Iris Saxer and/or Alfred L. Rosenberger Como mencioné antes, los tests sólo prueban (como máximo) que tu programa se va a comportar correctamente en un entorno exactamente igual al tuyo, y es mejor probarlo contra distintos ambientes de ejecución, para asegurarse de que funciona correctamente para una mayor cantidad de gente. Esto es más importante para aplicaciones "de escritorio" que para servers. Si las instrucciones de instalación de un server incluyen "necesita pirucho 1.4"... bueno, se consigue uno y se instala, aunque sea sólo para esa aplicación. Los deployments en servers suelen hacerse así, tratando de satisfacer los pedidos de lo que estás instalando. Pero si queremos decir "funciona con módulo X versiones Y y Z"... tenemos que por lo menos correr los tests contra esas versiones. Ya expliqué que ``virtualenv`` es una manera de hacer eso. Por favor, decíme que mientras leías eso pensabas "¡claro, puedo hacer un script que me arme los virtualenvs y corra los tests!" [#]_ .. [#] Si no lo pensaste.... *vergüenza debería darte* ;-) Por otro lado, es obvio que alguien tiene que haberlo pensado. Y alguien tiene que haberlo escrito. Y alguien tiene que haberlo publicado como open source. Y sí, ese alguien es el autor de `Tox `__, una herramienta para automatizar la creación de virtualenvs y la corrida de tests en los mismos. ¡Y está buena! Supongamos que queremos probar los tests de nuestro traductor al rosarino (``gaso4.py``)con python 2 y python 3. Lo primero que vamos a necesitar es un ``setup.py``. Lamentablemente, explicar como crear uno es tarea para más adelante en el libro, pero vamos a crear uno *muy* sencillito. .. class:: titulo-listado setup.py .. class:: listado .. code-block:: python :linenos: :linenos_offset: :include: codigo/4/setup.py Luego creamos un archivo ``tox.ini`` que le dice a Tox que necesitamos: .. class:: titulo-listado tox.ini .. class:: listado .. code-block:: ini :linenos: :linenos_offset: :include: codigo/4/tox.ini Y al ejecutar ``tox``, primero crea un "paquete" de nuestro módulo:: [ralsina@archie 4]$ tox _____________________________ [tox sdist] _____________________________ [TOX] ***creating sdist package [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4$ /usr/b in/python2 setup.py sdist --formats=zip --dist-dir .tox/dist >.tox/log/ 0.log [TOX] ***copying new sdistfile to '/home/ralsina/.tox/distshare/gaso4-1 .0.zip' Luego crea un ``virtualenv`` con python 2.7:: _________________________ [tox testenv:py27] __________________________ [TOX] ***creating virtualenv py27 [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox$ / usr/bin/python2.7 ../../../../../../../../usr/lib/python2.7/site-packag es/tox-1.1-py2.7.egg/tox/virtualenv.py --distribute --no-site-packages py27 >py27/log/0.log [TOX] ***installing dependencies: nose [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/py 27/log$ ../bin/pip install --download-cache=/home/ralsina/Desktop/proye ctos/python-no-muerde/codigo/4/.tox/_download nose >1.log [TOX] ***installing sdist [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/py 27/log$ ../bin/pip install --download-cache=/home/ralsina/Desktop/proye ctos/python-no-muerde/codigo/4/.tox/_download ../../dist/gaso4-1.0.zip >2.log Y ejecuta los tests (exitosamente):: [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4$ .tox/p y27/bin/nosetests gaso4.py .... ---------------------------------------------------------------------- Ran 4 tests in 0.016s OK Hace lo mismo con python 3.2:: _________________________ [tox testenv:py32] __________________________ [TOX] ***creating virtualenv py32 [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox$ / usr/bin/python3.2 ../../../../../../../../usr/lib/python2.7/site-packag es/tox-1.1-py2.7.egg/tox/virtualenv.py --no-site-packages py32 >py32/lo g/0.log [TOX] ***installing dependencies: nose [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/py 32/log$ ../bin/pip install --download-cache=/home/ralsina/Desktop/proye ctos/python-no-muerde/codigo/4/.tox/_download nose >1.log [TOX] ***installing sdist [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/py 32/log$ ../bin/pip install --download-cache=/home/ralsina/Desktop/proye ctos/python-no-muerde/codigo/4/.tox/_download ../../dist/gaso4-1.0.zip >2.log Pero los tests fallan miserablemente:: [TOX] /home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4$ .tox/ py32/bin/nosetests gaso4.py E ====================================================================== ERROR: Failure: SyntaxError (invalid syntax (gaso4.py, line 21)) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/p y32/lib/python3.2/site-packages/nose/failure.py", line 37, in runTest raise self.exc_class(self.exc_val).with_traceback(self.tb) File "/home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/p y32/lib/python3.2/site-packages/nose/loader.py", line 390, in loadTest sFromName addr.filename, addr.module) File "/home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/p y32/lib/python3.2/site-packages/nose/importer.py", line 39, in importF romPath return self.importFromDir(dir_path, fqname) File "/home/ralsina/Desktop/proyectos/python-no-muerde/codigo/4/.tox/p y32/lib/python3.2/site-packages/nose/importer.py", line 86, in importF romDir mod = load_module(part_fqname, fh, filename, desc) File "", line None SyntaxError: invalid syntax (gaso4.py, line 21) ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (errors=1) [TOX] ERROR: InvocationError: '.tox/py32/bin/nosetests gaso4.py' Y al final, un resumen:: ____________________________ [tox summary] ____________________________ [TOX] py27: commands succeeded [TOX] ERROR: py32: commands failed Cosas que no tuve que hacer para cada ``virtualenv``: * Crearlo y/o activarlo. * Copiar mi código. * Instalar dependencias. * Correr los tests manualmente. * Juntar los resultados de cada corrida de tests. Si bien cada paso es relativamente sencillo, son muchos. Y Tox automatiza todo. Testear todo el tiempo: Sniffer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Cita copada aquí -- Yo Integración continua: Jenkins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Cita copada aquí -- Yo Documentos, por favor ~~~~~~~~~~~~~~~~~~~~~ Desde el principio de este capítulo estoy hablando de testing. Pero el título del capítulo es "Documentación y Testing"... ¿Dónde está la documentación? Bueno, la documentación está infiltrada, porque venimos usando doctests en docstrings, y resulta que es posible usar los doctests y docstrings para generar un bonito manual de referencia de un módulo o un API. Si estás documentando un programa, en general documentar el API interno sólo es útil en general para el desarrollo del mismo, por lo que es importante pero no de vida o muerte. Si estás documentando una biblioteca, en cambio, documentar el API **es** de vida o muerte. Si bien hay que añadir un documento "a vista de pájaro" que explique qué se supone que hace uno con ese bicho, los detalles son fundamentales. Consideremos nuestro ejemplo ``gaso3.py``. Podemos verlo como código con comentarios, y esos comentarios como explicaciones con tests intercalados, o... podemos verlo como un manual con código adentro. Ese enfoque es el de "Literate programming" y hay bastantes herramientas para eso en Python, por ejemplo: `PyLit `_ Es tal vez la más "tradicional": podés convertir código en manual y manual en código. Ya no desde el lado del Literate programming, sino de un enfoque más habitual en Java o C++: `epydoc `_ Es una herramienta de extracción de docstrings, los toma y genera un sitio con referencias cruzadas, etc. `Sphinx `_ Es en realidad una herramienta para hacer manuales. Incluye una extensión llamada autodoc que hace extracción de docstrings. Hasta hay un módulo en la biblioteca standard llamado ``pydoc`` que hace algo parecido. A mí me parece que los manuales creados exclusivamente mediante extracción de docstrings son áridos, generalmente de tono desparejo y con una tendencia a carecer de cohesión narrativa, pero bueno, son exhaustivos y son "gratis" en lo que se refiere a esfuerzo, así que peor es nada. Combinando eso con que los doctests nos aseguran que los comentarios no estén completamente equivocados... ¿Cómo hacemos para generar un bonito manual de referencia a partir de nuestro código? Usando ``epydoc``, por ejemplo:: $ epydoc gaso3.py --pdf Produce este tipo de resultado: .. figure:: gaso3-api.pdf#page=2#viewrect=50,75,500,300 :width: 100% PDF producido por epydoc. También genera HTML. No recomendaría usar Sphinx a menos que lo uses como herramienta para escribir otra documentación. Usarlo sólo para extracción de docstrings me parece mucho esfuerzo para poca ganancia [#]_. .. [#] ¿Pero como herramienta para crear el manual y/o el sitio? ¡Es buenísimo! Igual que con los tests, esperar para documentar tus funciones es una garantía de que vas a tener un déficit a remontar. Con un uso medianamente inteligente de las herramientas es posible mantener la documentación "siguiendo" al código, y actualizada.