Contenido

Python Tornado: Web Testing

Una vez tenemos los conocimientos básicos de Tornado, lo lógico es seguir añadiendo características del framework.

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

Python

Recordando…

Vamos a probar el ejercicio que hicimos, así que aquí va el código de nuevo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# file: tornadohelloworld.py

import tornado.httpserver
import tornado.ioloop
import tornado.web
from tornado.options import define, options

class HelloworldHandler(tornado.web.RequestHandler):
    def get(self, name):
        if not name:
            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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# file: tornadohelloworld.py

import tornado.httpserver
import tornado.ioloop
import tornado.web
from tornado.options import define, options

class HelloworldHandler(tornado.web.RequestHandler):
    def get(self, name):
        if not name:
            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.

Testing básico

Pego el código. Luego lo explico:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# file: tornadohelloworld-test.py

from tornado.testing import AsyncHTTPTestCase
from tornado.testing import get_unused_port
import tornado.web
from tornadohelloworld import handlers
from tornadohelloworld import HelloworldHandler


class HelloworldHandlerTest(AsyncHTTPTestCase):
    def get_app(self):
        return tornado.web.Application(handlers)

    def test_empty(self):
        response = self.fetch("/")
        self.assertEqual(200, response.code)
        self.assertEqual('Hello, world!', response.body)

    def test_with_parameters(self):
        response = self.fetch("/MagMax")
        self.assertEqual(200, response.code)
        self.assertEqual('Hello, MagMax!', response.body)

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# file: tornadohelloworldpost.py

import tornado.httpserver
import tornado.ioloop
import tornado.web
from tornado.options import define, options

class HelloworldHandler(tornado.web.RequestHandler):
    def post(self):
        name = self.get_argument('name', None)
        if not name:
            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()

Y el test, claro:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# file: tornadohelloworldpost-test.py

from tornado.testing import AsyncHTTPTestCase
from tornado.testing import get_unused_port
import tornado.web
from tornadohelloworldpost import handlers
from tornadohelloworldpost import HelloworldHandler
from urllib import urlencode


class HelloworldHandlerTest(AsyncHTTPTestCase):
    def get_app(self):
        return tornado.web.Application(handlers)

    def test_empty(self):
        response = self.fetch("/", method='POST', body='')
        self.assertEqual(200, response.code)
        self.assertEqual('Hello with POST, world!', response.body)

    def test_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!