Tests en Django. Como escribir software que dura en el tiempo

In the last pybirras-tenerife I gave a brief talk about testing Django apps, and I promised to publish a post in spanish with more in-depth info about that. So if you can’t read spanish I apologize for the inconvenience.

Dicho lo cual, al tajo.

Tests en Django

Django tiene su propio sistema de tests integrado, básicamente se trata de una extensión de unittest. Para ser justos también se pueden escribir “Doctests”, pero esto es algo que nunca me ha resultado útil así que no hablaré de ello.Por tanto los tests en django son relativamente fáciles de llevar a cabo. A continuación incluyo un TestCase muy simple que espero sirva como ejemplo

Unittest: Los tests más simples

from django.test import TestCase

class SomeTest(TestCase):
    fixtures = ['the_data']

    def setUp(self):
        self.foo = Bar()

    def tearDown(self):
        self.foo.close()

    def test_emptylist_is_false(self):
        self.assertTrue([] == False)

Este código tendrá que estar dentro del módulo tests de alguna aplicación instalada. Para ejecutar este test lo suyo sería ejecutar el siguiente código

$ python manage.py test <nombre_de_la_app>

Vale ya tenemos un test, pero esto no sirve para mucho. Los tests son muy útiles, pero si tenemos que escribirlos así, también son un tostón. ¿Cuáles son los principales problemas?. En mi opinión el principal problema de escribir los tests así son los datos.

Los datos

El problema es que las fixtures no son muy manejables, tienen referencias a claves concretas (aunque esto puede ser mitigado con las clave naturales), y si necesitas un conjunto de datos complejos es difícil escribirlas a mano.

Una alternativa mejor es utilizar factory_boy. De esto he hablado en una entrada anterior. Básicamente se trata de disponer de una factoría que genera objetos de base de datos asignando valores predeterminados a los atributos que no se suministran.

class RolFactory(factory.django.DjangoModelFactory):
    FACTORY_FOR = Rol

    name = factory.Sequence(lambda n: u"rol%s" % n)
    description = u"A test rol";

class CacheFactory(factory.django.DjangoModelFactory):
    FACTORY_FOR = Cache

    id = factory.Sequence(lambda n: n)
    code = factory.Sequence(int)
    document_number = factory.Sequence(lambda n: u"%s" % n)
    name = u"John"
    surname = u"Doe";

r = RolFactory.create(name="alumno")
CacheFactory.create(rol=r)

Como vemos en el código anterior, se pueden crear los datos que necesitamos para nuestros tests de una manera mucho más simple. Además esto tiene la ventaja de que si el modelo fuera modificado, solo tendríamos que adaptar nuestra factoría y el resto de los tests seguiría funcionando igual.

Por supuesto podemos utilizar esta librería en nuestros tests en Django sin ningún problema.

El siguiente problema sería como testear código que no sé como se comporta. Esto es mucho más habitual es proyectos “brownfield” que en proyectos “greenfield”, lo que ocurre es que todos en algún momento tenemos que afrontar este problema.

El código legado

Antes hemos visto que utilizábamos una clase hija de “TestCase”, en realidad existen más clases base, pero creo que la más interesante es “LiveServerTestCase”. Esta clase lo que hace es que “levanta” un servidor local durante la fase de testeo para que pueda utilizar un navegador externo en lugar de tener que utilizar el cliente de django. La principal ventaja es que podemos utilizar Selenium IDE para crear nuestro test y luego exportarlo a python (es una de las opciones de Selenium IDE). Este tipo de tests tiene un problema, son lo que llamamos tests débiles, desde que cambies la cosa más mínima el test se “romperá”. Por contra son muy útiles para asegurarse de que las cosas funcionan como antes, y creo que se puede asumir la debilidad para este tipo de casos.

from django.test import LiveServerTestCase
from selenium import webdriver
#from pyvirtualdisplay import Display


class BuscarTests(LiveServerTestCase):
    fixtures = ['tests', ]

    def setUp(self):
        super(BuscarTests, self).setUp()
        #self.display = Display(size=(1280, 1024))
        #self.display.start()

    def tearDown(self):
        #self.display.stop()
        super(BuscarTests, self).tearDown()

    def test_buscar(self):
        browser = webdriver.Firefox()
        browser.get(self.live_server_url + "/buscar/")
        browser.find_element_by_id("id_query").clear()
        browser.find_element_by_id("id_query").send_keys("Juan")
        browser.find_element_by_css_selector("button[type="submit"]").click()
        element = browser.find_element_by_tag_name('h1')
        assert element.text == 'Juan'
        browser.close()

En el anterior código es de reseñar que las líneas comentadas permitirían pasar este test en un entorno “headless”, lo cuál es interesante si tenemos un sistema de integración continua.

Por otro lado hay veces en las que no queremos o no podemos testear un elemento. En una de las aplicaciones que desarrollo tengo un método que hace unas llamadas a procedimientos almacenados en base de datos. En el entorno de desarrollo/pruebas no dispongo del sistema que corre los procedimientos almacenado, por tanto no puedo permitir que se ejecute dicho código. ¿Cómo hago para hacer un test de eso?

Doubles

Los doubles son objetos que se hacen pasar por otros objetos. Los “mocks”, “stubs” y “spys” son algunos de los tipos de doubles. En python hay numerosas librerías para esto, pero hay una que ha sido incluida en la librería estándar en la versión 3.3 (en versiones previas está disponible con librería de terceros). En el siguiente ejemplo se comprueba que se ha llamado a una función.

    @patch.object(Persona, "call_stored_proc")
    def test_metodo_called_when_obj_created(self, mock_method):
        n = Obj(numero=5,
                string="PAS")
        n.save()
        self.assertTrue(mock_method.called)

En este caso hay una listener vinculado a la señal post_save de la clase “Obj” que sólo llama al método “call_stored_proc” bajo determinados circunstancias.

Otra forma muy común de utilizar esta librería es modificando los métodos que deseemos para que devuelvan una salida concreta.

from mock import Mock

a = Foo()
a.bar = Mock(return_value=5)

Por supuesto los tests en Django son un tema que da para mucho, pero hoy lo vamos a dejar por aquí. En una próxima entrada hablaré sobre una maravilla llamada behave que permite hacer BDD de una manera cómoda y sencilla y también sobre como integrar todas estas cosas con Jenkins.