En este caso voy a añadir una de las características más importantes: el entorno de pruebas. En mi opinión, es siempre lo primero que se debería enseñar, mucho antes de otras cosas como el sistema de location (idiomas, etc.), acceso a servicios de terceros (google, facebook, twitter, oauth, …) y otras cosas molonas.
Partiremos de los conocimientos anteriores y pondremos un ejemplo sencillo de uso de esta herramienta.
Se tratará de pruebas de integración. No se utilizará un Browser, y podremos mockear lo que nos haga falta (aunque en este artículo no se utilizarán mocks).
Recordando…
Vamos a probar el ejercicio que hicimos, así que aquí va el código de nuevo:
#!/usr/bin/env python# -*- coding: utf-8 -*-# file: tornadohelloworld.pyimporttornado.httpserverimporttornado.ioloopimporttornado.webfromtornado.optionsimportdefine,optionsclassHelloworldHandler(tornado.web.RequestHandler):defget(self,name):ifnotname:name='world'self.write("Hello, {}!".format(name))define('port',default=8000,help='Port to be used')tornado.options.parse_command_line()handlers=[(r"/(.*)",HelloworldHandler),]application=tornado.web.Application(handlers)server=tornado.httpserver.HTTPServer(application)server.listen(options.port)tornado.ioloop.IOLoop.instance().start()
Y vamos a crear dos pruebas muy básicas: Si no accedemos con parámetros, se mostrará por pantalla “Hello, world!”, pero si le pasamos un parámetro “MagMax”, se mostrará “Hello, MagMax!”. Veámoslo:
Unas modificaciones
Lo primero que tenemos que hacer son unas pequeñas modificaciones a nuestro programa. Si no las hacemos, cuando el test runner cargue nuestro programa entrará en el bucle de eventos y nunca terminará, así que es necesario indicar que no queremos que se produzca este imprevisto:
#!/usr/bin/env python# -*- coding: utf-8 -*-# file: tornadohelloworld.pyimporttornado.httpserverimporttornado.ioloopimporttornado.webfromtornado.optionsimportdefine,optionsclassHelloworldHandler(tornado.web.RequestHandler):defget(self,name):ifnotname:name='world'self.write("Hello, {}!".format(name))define('port',default=8000,help='Port to be used')handlers=[(r"/(.*)",HelloworldHandler),]if__name__=='__main__':tornado.options.parse_command_line()application=tornado.web.Application(handlers)server=tornado.httpserver.HTTPServer(application)server.listen(options.port)tornado.ioloop.IOLoop.instance().start()
Lo conseguimos mediante el típico bloque if __name__ == '__main__'. Podéis observar que he dejado fuera la definición de las opciones y los handlers. Es necesario que las opciones estén definidas o todo fallará si se intenta acceder a ellas. Y los handlers pueden sernos útiles, ya que es justamente lo que queremos probar.
Como puede observarse, las modificaciones son mínimas.
Aquí hay algo de magia. Pero en cuanto la explique, veréis como todo es estupendo y maravilloso.
La clase AsyncHTTPTestCase hereda de unittest.TestCase, por lo que tendremos todos los métodos a los que estamos acostumbrados: assertEqual, assertIn,… Esto requiere que, si usáis los métodos setUp o tearDown, tendréis que invocar al padre, por si AsyncHTTPTestCase está haciendo de las suyas.
Además del setUp, AsyncHTTPTestCase llamará a un método especial, get_app, que es quien debe devolver un objeto de tipo tornado.web.Application. Éste es nuestro servidor, y se lanzará en un puerto diferente para cada test, asegurando un aislamiento total de los tests.
La clase AsyncHTTPTestCase también nos proporciona un método fetch que nos obtiene la URL que necesitamos y nos devuelve un tornado.httpclient.HTTPResponse.
Una vez aquí, lo típico es preguntarse cómo se realiza un test sobre POST… Y es exactamente igual:
Testing mediante POST
Lo primero, veamos lo que queremos probar, que consistirá en el mismo ejemplo de antes, pero mediante POST:
#!/usr/bin/env python# -*- coding: utf-8 -*-# file: tornadohelloworldpost.pyimporttornado.httpserverimporttornado.ioloopimporttornado.webfromtornado.optionsimportdefine,optionsclassHelloworldHandler(tornado.web.RequestHandler):defpost(self):name=self.get_argument('name',None)ifnotname:name='world'self.write("Hello with POST, {}!".format(name))define('port',default=8000,help='Port to be used')handlers=[(r"/",HelloworldHandler),]if__name__=='__main__':tornado.options.parse_command_line()application=tornado.web.Application(handlers)server=tornado.httpserver.HTTPServer(application)server.listen(options.port)tornado.ioloop.IOLoop.instance().start()
#!/usr/bin/env python# -*- coding: utf-8 -*-# file: tornadohelloworldpost-test.pyfromtornado.testingimportAsyncHTTPTestCasefromtornado.testingimportget_unused_portimporttornado.webfromtornadohelloworldpostimporthandlersfromtornadohelloworldpostimportHelloworldHandlerfromurllibimporturlencodeclassHelloworldHandlerTest(AsyncHTTPTestCase):defget_app(self):returntornado.web.Application(handlers)deftest_empty(self):response=self.fetch("/",method='POST',body='')self.assertEqual(200,response.code)self.assertEqual('Hello with POST, world!',response.body)deftest_with_parameters(self):response=self.fetch("/",method='POST',body=urlencode({'name':'MagMax'}))self.assertEqual(200,response.code)self.assertEqual('Hello with POST, MagMax!',response.body)
Conclusión
Hay otros frameworks de testing, pero si usáis Tornado, resulta muy sencillo no utilizar nada más. Tornado nos ofrece todo lo que podamos necesitar, aunque sin florituras. Si queremos florituras, podemos usar otros frameworks como django.
Personalmente, django me agobia. Demasiada magia. Y me gusta Tornado por su simplicidad y versatilidad. Me deja utilizar las herramientas que me proporciona, pero puedo decidir usar cualquier otra.
De todas maneras, uséis lo que uséis, ¡Escribid pruebas!