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

Jesus Cea jcea en jcea.es
Jue Abr 24 00:54:30 CEST 2014


Repito el mensaje completo porque han pasado casi 5 meses desde el hilo
original y habrá gente que no lo haya leído.

Esta vez pongo el comentario aquí.

No tengo nada que decir a tu mensaje, Lasi. Magnífico también.

Efectivamente, encontrar el equilibrio en hasta dónde llegar con los
tests es difícil, una tarea de nunca acabar. El cliente quiere probar
todo lo imaginable hasta que le pasas el presupuesto :-).

En fin, un buen hilo que no acabé de cerrar. Lo que puedo decir es que
ha cambiado mi forma de escribir código, al menos cuando pretendo que
ese código sea mantenible en el futuro.



On 06/01/14 03:01, lasizoillo wrote:
> 
> DISCLAIMER: Hago muchisimo menos testing del que debería.
> 
> El 31 de diciembre de 2013, 10:06, Jesus Cea <jcea en jcea.es
> <mailto:jcea en jcea.es>> escribió:
> 
> 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
>> <http://gist.github.com> o a pastebin.com <http://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.
> 
>     _______________________________________________
>     Python-es mailing list
>     Python-es en python.org <mailto:Python-es en python.org>
>     https://mail.python.org/mailman/listinfo/python-es
>     FAQ: http://python-es-faq.wikidot.com/
> 
> 
> 
> 
> _______________________________________________
> Python-es mailing list
> Python-es en python.org
> https://mail.python.org/mailman/listinfo/python-es
> FAQ: http://python-es-faq.wikidot.com/
> 

-- 
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

------------ próxima parte ------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 538 bytes
Desc: OpenPGP digital signature
URL: <http://mail.python.org/pipermail/python-es/attachments/20140424/ecfaa5f2/attachment.sig>


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