DISCLAIMER: Hago muchisimo menos testing del que debería. El 31 de diciembre de 2013, 10:06, Jesus Cea <[email protected]> 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 _/_/ _/_/_/ _/_/_/ > [email protected] - http://www.jcea.es/ _/_/ _/_/ _/_/ _/_/ _/_/ > Twitter: @jcea _/_/ _/_/ _/_/_/_/_/ > jabber / xmpp:[email protected] _/_/ _/_/ _/_/ _/_/ _/_/ > "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 > [email protected] > https://mail.python.org/mailman/listinfo/python-es > FAQ: http://python-es-faq.wikidot.com/ >
_______________________________________________ Python-es mailing list [email protected] https://mail.python.org/mailman/listinfo/python-es FAQ: http://python-es-faq.wikidot.com/
