[Python-es] ¿Algún recurso sobre trucos prácticos y ejemplos realistas de testing?

lasizoillo lasizoillo en gmail.com
Lun Ene 6 03:01:28 CET 2014


DISCLAIMER: Hago muchisimo menos testing del que debería.

El 31 de diciembre de 2013, 10:06, Jesus Cea <jcea en jcea.es> escribió:

> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> Una cosa que he ido descubriendo con los años es que para poder hacer
> buenos tests es conveniente que lo que estés probando se preste a
> ello. Tienes que programar de forma que lo que haces sea fácilmente
> testable.
>

Creo que la respuesta de andrey va en esa linea y me parece genial ;-)


>
> El enfoque habitual en otros lenguajes, y mi tendencia en el pasado es
> utilizar inyección de dependencias. De hecho hubo una época en la que
> di la lata un poco para incluir inyección de dependencias en Python,
> pero la respuesta masiva fue que eso era antipitónico y que el futuro
> eran los mocks y similares.
>

Un sistema monolítico es difícil de depurar, pero un sistema demasiado
modularizado es difícil de configurar (en algún lado tiene que estar cuales
son las dependencias a inyectar o la implementación de los plugins a usar o
...). Con un sistema muy modular terminas necesitando pruebas de
integración para probar la configuración.

Aparte de eso, el código que se inyecta en las pruebas también hay que
programarlo. Con los mocks solo programas una  parte específica en plan
reactivo, que es mucho más simple que definir una interfaz común y hacer
diferentes implementaciones.

En django, por ejemplo, algunos módulos tienen versiones orientadas al
desarrollo para que puedas testear más fácil o evites tener que configurar
entornos completos en las máquinas de desarrollo. Por ejemplo con el envio
de correo https://docs.djangoproject.com/en/dev/topics/email/

Al final esto es todo muy budista y la solución está en el camino del medio.



> Pero me encuentro que para probar una rutina de 20 lineas escribo 200
> lineas de tests más complejos que la propia rutina a comprobar, el
> desarrollo es lento y no puedo evitar pensar que estoy haciendo algo
> mal :-).
>
>
Como chascarrillo fácil: reescribiendo el código para que sea más fácil de
probar vas a tener 200 líneas de código y 20 líneas de test. Así que igual
no es el mejor de los indicadores para saber si estas haciendo algo bien o
mal ;-)

¿Cuál es el objetivo del test?

Si es encontrar todos los posibles errores que puede tener una parte del
programa es normal que el test sea muy largo. No bastaría con una cobertura
de código del 100%, habría que ir más a por una cobertura de branches del
100%. Y tener mucha imaginación (¿que pasaría si tu programa se ejecuta en
un freebsd con las interfaces em0 y ath0 y ninguna eth0?)

Otro enfoque más optimista es definir un contrato mínimo del código a
probar y cuando surjan bugs ir añadiendo esos casos y tener los tests para
evitar problemas de regresión. Un caso extremo es no hacer testing porque
somos extremadamente optimistas con que todo vaya a ir bien.

Otro enfoque es que ayuda a desarrollar más rápido. En vez escribir a lo
loco y luego ver dónde y por qué falla (nunca funciona a la primera) se van
haciendo los test para avanzar despacito, pero seguro. Este enfoque falla
cuando las especificaciones cambian, porque hay que tocar el código y el
test a la vez.

A veces pienso que dar con el enfoque correcto tiene mucha parte de suerte
o del caso concreto :-(



> Así que, ¿alguien conoce recursos online con consejos prácticos y
> ejemplos realistas?. Porque hacer un chequeo mínimamente completo de
> esta rutina está siendo un infierno.
>

El enfoque realista para hacer pruebas creo que consiste en prueba y error
:-)

Los consejos de andrey de separar me parecen la clave. Otra aproximación es
delegar todo lo posible en código que ya este probado, cuanto menos se
escriba menos posibilidades de cometer errores hay.


>
> Esta rutina genera una clave al azar e intenta registrarla en un
> servidor, que devuelve un 401 mientras un administrador no ha admitido
> el registro (y en ese caso devuelve un 200). Una vez que tenemos el
> 200, nos guardamos ese usuario y clave en disco y no necesitamos
> repetir la operación.
>

Voy a poner comentarios en el código con preguntas y alguna respuesta
prefijados con "# lasi:"


>
> """
> def consigue_autenticacion() :
>     # Creamos el fichero si es preciso
>     open("/tmp/heartbeat", "w").close()
>
       # lasi: Qué pasa si otro programa usa el mismo fichero puesto a
fuego. Debería llevar el pid el nombre de este fichero? Se debería usar el
modulo tempfile?

>     #os.utime("/tmp/heartbeat")
>
>     try :
>         with open("/local/auth", "r") as f :
>
               # lasi: Qué pasa si no se puede usar ese nombre de fichero
porque no existe /local o no se tienen permisos?

>             token = f.read().strip()
>         if " " in token :
>
               # lasi: Asumimos que nadie va a tocar a mano y joder el
formato?

>             return  # Ya tenemos usuario y clave
>     except FileNotFoundError :
>         with open("/dev/urandom", "rb") as f :
>
               # lasi: asumimos que pasamos de windows.
http://docs.python.org/dev/library/os.html#os.urandom

>             token = f.read(4096)
>         if len(token) != 4096 :
>             raise RuntimeError("Lectura parcial de entropía")
>         token = md5(token).hexdigest()
>         with open("/local/auth", "w") as f :
>             f.write(token+"\n")
>
               # lasi: para que escribir una información inválida que no se
va a usar?
               # lasi: Es para forzar un fallo si no se puede escribir el
fichero? Tendria sentido pero necesita comentario para evitar tentaciones
de borrar esas dos lineas.

>
>     usuario = "XXXXXX"
>     clave = "XXXXXXXX"  # Confidencial, pero no crítico
>     auth = requests.auth.HTTPBasicAuth(usuario, clave)
>
>     addr = netifaces.ifaddresses("eth0")
>     ip_addr = addr[netifaces.AF_INET][0]["addr"]
>     mac_addr = addr[netifaces.AF_LINK][0]["addr"]
>     mac_addr = mac_addr[0:2]+mac_addr[3:5]+mac_addr[6:8]+ \
>                 mac_addr[9:11]+mac_addr[12:14]+mac_addr[15:17]
>

       # lasi: Posiblemente más seguro
http://docs.python.org/dev/library/uuid.html#uuid.getnode
       # lasi: Para que las pruebas sean realmente unitarias mejor separar
en una "unidad" distinta y mejor para elegir la mejor implementación (si es
que eso existe) ;-)

>
>     factor = 1*60
>     while True :
>
           """ lasi: Testear posibles bucles infinitos puede alargarse
mucho :-( Se puede generalizar:

class FunctionalException(Exception):
    pass

def reintentar(func):
    # No tan facil de probar
    def inner(*args, **kwargs):
         retry = True
         while retry:
              try:
                    return func(*args, **kwargs)
              except FunctionalException:
                    # Ejecutar código de espera y demás historias
                    ...
                    continue
              except Exception as e:
                     raise e
     return inner

def _consigue_autenticacion():
    # Facil de probar
    ... # codigo de arriba
    result = autenticar(fulanito, menganito)
    if result == OK:
        return
    if result == NOT_AUTH:
        raise FunctionaException("No autenticado")
    raise RuntimeError("bla, bla, bla")


consigue_autenticacion = reintentar(_consigue_autenticacion)

           """

>         t = time.time()
>         respuesta = requests.get("https://XXXXXX.jcea.es/registro?"
>                 "ip_addr=%s&mac_addr=%s&ts=%.0f" \
>                         %(ip_addr, mac_addr, time.time()),
>              auth = auth,
>              verify = "XXXXXXXXX.jcea.es.cert",
>              timeout = 1*60,
>              headers = {"clave": token})
>         os.utime("/tmp/heartbeat")
>         if respuesta.status_code == 401 :
>             t = t + factor
>             factor = factor * 3
>             if factor > 3600 :
>                 factor = 3600
>             while time.time() < t :
>                 time.sleep(10)
>                 os.utime("/tmp/heartbeat")
>             continue  # Volvemos a intentarlo
>         elif respuesta.status_code == 200 :
>             with open("/local/auth", "w") as f :
>                 f.write(mac_addr+" "+token+"\n")
>             return
>         else :
>             raise RuntimeError("El servidor nos devuelve un status %s" \
>                     %respuesta.status_code)
> """
>
> El código de testeo de esta rutina es complicado de cojones, feo,
> frágil. Uso Mocks para comprobar que las llamadas se realizan en el
> orden y con los parámetros correctos, tiro excepciones, simulo
> ficheros, etc. La pruebo a fondo. Pero desarrollar el test ha sido
> costosísimo.


Sería interesante que subieras a un gist.github.com o a pastebin.com el
código del test también. Fijo que contiene cosas muy curiosas.


>
> ¿Consejos?.
>

Peer review siempre que se pueda. Muchos fallos de programación vienen de
corner cases que no se han tenido en cuenta a la hora de programar y que no
se vuelven a tener en cuenta a la hora de programar el test.

Tener mala baba haciendo pruebas. ¿A quién se le podría ocurrir probar si
un unicode se va a comportar como un número
http://en.wikipedia.org/wiki/Numerals_in_Unicode? Creo que solo el que ha
visto alguna vez un bug de ese tipo. Y así todo el rato: meter secuencias
de escape, signos usados en formatos, tamaños muy grandes o muy pequeños,
...

Separar siempre que se pueda para que los tests sean unitarios. Y luego un
test de integración para probarlos en su conjunto. Es mucho más fácil
probar por separado y refactorizar una función que se llama _get_mac() que
unas lineas incrustadas en la función consigue_autorizacion()

Hacer implementaciones dummy de cosas que acceden a sistemas externos que
no puedas tener montadas en el entorno de desarrollo (sobre todo si te
gusta programar en aburridos viajes de autobús por la meseta castellana sin
internet). Ya sean usados mediante inyección de dependencias, sistema de
plugins o lo que sea.

Valorar costes/beneficios de los diferentes tests (chema apuntaba varias
herramientas) y dentro de cada uno. A veces algo como behave[1] te viene
bien para probar las cosas y de paso tener documentación funcional
actualizada del proyecto, a veces unittest es más apropiado porque
realmente haces cosas unitarias y es más directo, ... Bajar al detalle esta
muy bien, pero nadie prueba el que va a pasar si al sumar dos cadenas se
produce un MemoryError, en algún lado hay que parar.

[1] https://pypi.python.org/pypi/behave/1.2.3


Un saludo y espero que esto de como para un buen rato de charla y cañas :-)

Javi


> Se admiten recomendaciones de libros.
>
> - --
> Jesús Cea Avión                         _/_/      _/_/_/        _/_/_/
> jcea en jcea.es - http://www.jcea.es/     _/_/    _/_/  _/_/    _/_/  _/_/
> Twitter: @jcea                        _/_/    _/_/          _/_/_/_/_/
> jabber / xmpp:jcea en jabber.org  _/_/  _/_/    _/_/          _/_/  _/_/
> "Things are not so easy"      _/_/  _/_/    _/_/  _/_/    _/_/  _/_/
> "My name is Dump, Core Dump"   _/_/_/        _/_/_/      _/_/  _/_/
> "El amor es poner tu felicidad en la felicidad de otro" - Leibniz
> -----BEGIN PGP SIGNATURE-----
> Version: GnuPG v1.4.15 (GNU/Linux)
> Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/
>
> iQCVAwUBUsKJG5lgi5GaxT1NAQLu7wQAlG+qzPwUIZWi0wLtgXBg44WWMbODuqZd
> SQrsSxFxEL5GHhyWiW2RCJ7cG5B9Sgtbfg2Sez0o9PiAwFxMku42DxTJwS/tPTpK
> 15I9WUuvN2lylAOvMPvn5CuUsuis2wQ0R2hv5jgXPJ39Kl/e2ncwuiZB83J1APvd
> 5jZXkbYoTz8=
> =9H+8
> -----END PGP SIGNATURE-----
> _______________________________________________
> Python-es mailing list
> Python-es en python.org
> https://mail.python.org/mailman/listinfo/python-es
> FAQ: http://python-es-faq.wikidot.com/
>
------------ próxima parte ------------
Se ha borrado un adjunto en formato HTML...
URL: <http://mail.python.org/pipermail/python-es/attachments/20140106/9b38eccf/attachment.html>


Más información sobre la lista de distribución Python-es