Curso Python. Volumen XIX: Framework Django. Parte XIV

Escrito por Javier Ceballos Fernández

Bienvenidos un día más al curso de Python, en este capítulo vamos continuar con las pruebas automáticas justo donde lo dejamos en el capítulo anterior. Vamos a implementar pruebas para nuestras vistas dentro de nuestra aplicación con el framework Django. Estas pruebas automáticas nos ayudarán a asegurarnos que nuestra aplicación funciona de manera correcta. Así que pongámonos manos a la obra.

Mejorando nuestras vistas

Nuestra lista de preguntas nos muestra entradas que no están publicadas todavía (i.e. aquellas que tienen “fecha_publi” en el futuro). Así que vamos a empezar por arreglar esto. Cuando estuvimos creando vistas, cambiamos las funciones de “view” por las genéricas de “ListView”:

polls/views.py

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):

        """Return the last five published questions."""
        return Question.objects.order_by('-fecha_publi')[:5]

“response.context_data[‘latest_question_list’]” extrae los datos que la “view” pone en el contexto. Nos dirigiremos al método “get_queryset” y lo modificaremos de modo que también compruebe la fecha, para hacer la comprobación tendremos que compararla con “timezone.now()”. Primero agregaremos hacer un “import”:

polls/views.py 
from django.utils import timezone

Y luego corregimos el método “get_queryset” de la siguiente manera:

polls/views.py

    def get_queryset(self):
        """ Return the last five published questions
        (not including those set to be published in the future). """
        return Pregunta.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-fecha_publi')[:5]

“Pregunta.objects.filter(fecha_publi__lte=timezone.now)” devuelve un “queryset” que contiene las instancias de “Pregunta” cuyo campo “fecha_publi” es menor o igual que “timezone.now”, es decir, la fecha de publicación es anterior o igual a la fecha actual.

Probando nuestra nueva vista

Una vez realizado los cambios podemos comprobar que la aplicación se comporta como deseamos, para ello tendremos que iniciar el servidor de desarrollo. Una vez iniciado accederemos a nuestra aplicación por el navegador. Después crearemos una “Pregunta” con fecha pasada, y otra con fecha futura y comprobamos si en el listado sólo vemos aquellas preguntas que han sido publicadas. Es cierto que esto no es una tarea que queramos estar repitiendo constantemente, por lo que vamos a crear una prueba para realizar esta comprobación.

Para crear la prueba tendremos que agregar lo siguiente a polls/tests.py:

polls/tests.py

from django.core.urlresolvers import reverse

Lo primero que vamos hacer es crear un método que nos permita crear preguntas, como así también una nueva clase de prueba:

polls/tests.py

def create_question(texto_pregunta, days):

    """
    Creates a question with the given `texto_pregunta` published the given
    number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(texto_pregunta= texto_pregunta,
        fecha_publi =time)

class QuestionViewTests(TestCase):

     def test_index_view_with_no_questions(self):

     """
     If no questions exist, an appropriate message should be displayed.
     """
     response = self.client.get(reverse('polls:index'))
     self.assertEqual(response.status_code, 200)
     self.assertContains(response, "No polls are available.")
     self.assertQuerysetEqual(response.context['latest_question_list'], [])

def test_index_view_with_a_past_question(self):

     """
     Questions with a pub_date in the past should be displayed on the
     index page.
     """
     create_question(texto_pregunta ="Past question.", days=-30)
     response = self.client.get(reverse('polls:index'))
     self.assertQuerysetEqual(
         response.context['latest_question_list'],
         ['<Question: Past question.>'])

def test_index_view_with_a_future_question(self):

    """
    Questions with a fecha_publi in the future should not be displayed on
    the index page.
    """
    create_question(texto_pregunta ="Future question.", days=30)
    response = self.client.get(reverse('polls:index'))
    self.assertContains(response, "No polls are available.",
        status_code=200)
    self.assertQuerysetEqual(response.context['latest_question_list'], [])

def test_index_view_with_future_question_and_past_question(self):

    """
    Even if both past and future questions exist, only past questions
    should be displayed.
    """
    create_question(texto_pregunta ="Past question.", days=-30)
    create_question(texto_pregunta ="Future question.", days=30)
    response = self.client.get(reverse('polls:index'))
    self.assertQuerysetEqual(
        response.context['latest_question_list'],
        ['<Question: Past question.>'])

def test_index_view_with_two_past_questions(self):

    """
    The questions index page may display multiple questions.
    """
    create_question(texto_pregunta ="Past question 1.", days=-30)
    create_question(texto_pregunta ="Past question 2.", days=-5)
    response = self.client.get(reverse('polls:index'))
    self.assertQuerysetEqual(
        response.context['latest_question_list'],
        ['<Question: Past question 2.>', 
        '<Question: Past question 1.>']
)

Lo que acabamos de mostraros serían todas las pruebas, pero vamos a analizarlas en profundidad. Primero tenemos una función, “create_question”, para evitar la repetición en el proceso de crear preguntas:

“test_index_view_with_no_questions” no crea preguntas, pero comprueba el mensaje “No polls are available.” y verifica que “latest_question_list” es vacío. Fijaros que la clase “django.test.TestCase” nos proporciona algunos métodos adicionales que nos pueden ayudar como por ejemplo dos métodos que hemos utilizado “assertContains()” y “assertQuerysetEqual()”.

En “test_index_view_with_a_past_question”, creamos una pregunta y verificamos que aparece en el listado.

En “test_index_view_with_a_future_question”, creamos una pregunta con “fecha_publi” en el futuro. La base de datos se resetea para cada método de test, entonces la primera pregunta no está más, y entonces nuevamente no deberíamos tener ninguna entrada en el listado.

Y así sucesivamente. De este modo estamos usando las pruebas para imitar el uso que haría un usuario de nuestra aplicación, y así saber si la aplicación está actuando del modo que queremos en cada situación.

Probando DetailView

Por el momento el código que hemos agregado funciona como esperamos, sin embargo, aunque las encuestas futuras no aparecen en el “index”, un usuario todavía puede verlas si saben o si capaces de adivinar la URL correcta. Por lo que necesitamos restricciones similares para “DetailViews”, por lo que tendremos que hacer los cambios que os mostramos a continuación:

polls/views.py

class DetailView(generic.DetailView):

...

    def get_queryset(self):
         """
         Excludes any questions that aren't published yet.
         """
         return Pregunta.objects.filter(fecha_publi__lte=timezone.now())

No hace falta mencionar, que vamos a agregar más pruebas para comprobar que una Pregunta cuya “fecha_publi” es en el pasado se puede ver, mientras que una con “fecha_publi” en el futuro, no:

polls/tests.py

class QuestionIndexDetailTests(TestCase):

    def test_detail_view_with_a_future_question(self):
        """
        The detail view of a question with a pub_date in the future should
        return a 404 not found.
        """
        future_question = create_question(texto_pregunta='Future question.',
            days=5)
        response = self.client.get(reverse('polls:detail',
        args=(future_question.id,)))
        self.assertEqual(response.status_code, 404)

    def test_detail_view_with_a_past_question(self):
        """
        The detail view of a question with a fecha_publi in the past should
        display the question's text.
        """
        past_question = create_question(texto_pregunta='Past Question.',
            days=-5)
        response = self.client.get(reverse('polls:detail',
        args=(past_question.id,)))
        self.assertContains(response, past_question.texto_pregunta,
            status_code=200)

Ideas para otras pruebas

Es recomendable agregar un método “get_queryset” similar al de “ResultsView” y crear una nueva clase para las pruebas de esta vista. Sería muy similar a las ya presentadas, de hecho, habría bastante código que se repetiría.

También se podría mejorar nuestra aplicación de diversas maneras, agregando pruebas en el camino. Por ejemplo, no tiene mucho sentido que se permita publicar preguntas sin opciones. Entonces, nuestras vistas podrían comprobar esto, y excluir esas preguntas. Las pruebas crearían una instancia de Pregunta sin Opciones relacionadas, y luego verificarían que no se publica, también habría que hacer que creará una instancia de Pregunta con Opciones, para verificar que sí se publica.

Quizás los usuarios administradores conectados deberían poder ver preguntas no publicadas, pero los demás usuarios no. Una vez más: cualquier funcionalidad que necesite agregarse debe estar acompañada por las pruebas correspondientes, ya sea escribiendo primero la prueba y luego el código que lo hace pasar, o escribir el código de la funcionalidad primero y luego escribir la prueba correspondiente para probar dicha funcionalidad.

Llegará un punto en el que al ver tantas pruebas, uno se hace la pregunta de que si no habrá realizado demasiadas pruebas automáticas. Tratándose de pruebas automáticas, cuanto más mejor. Puede parecer que nuestras pruebas automáticas están creciendo fuera de control. A este ritmo pronto tendremos más código en nuestras pruebas que en nuestra aplicación. Pero esto no importa. En gran medida, uno escribe una prueba una vez y luego se olvida. Ésta va a seguir cumpliendo su función mientras uno continúa desarrollando su programa.

Algunas veces las pruebas automáticas van a necesitar ser actualizadas. Supongamos por ejemplo que corregimos nuestras vistas para que solamente se publiquen “Preguntas con Opciones”. En este caso, muchos de nuestras pruebas existentes van a fallar – diciéndonos qué pruebas tenemos que actualizar y corregir – así que hasta cierto punto las pruebas se pueden cuidarse ellas mismas.

Como mucho, mientras uno continúa desarrollando, se puede encontrar que hay algunas pruebas que se hacen redundantes. Incluso esta redundancia no es un problema, cuando se trata de probar, la redundancia es algo bueno.

Mientras que las pruebas automáticas estén organizadas de manera razonable, no se van a hacer inmanejables. Algunas buenas prácticas:

  • un “TestClass” separado para cada modelo o vista
  • un método de prueba separado para cada conjunto de condiciones a probar
  • nombres de método de prueba que describan su función

Pruebas adicionales

Sólo hemos presentado lo básico sobre pruebas. La realidad es que hay bastante más que se puede hacer, y existen herramientas muy útiles a disposición de los desarrolladores para lograr cosas muy interesantes.

Por ejemplo, mientras que nuestras pruebas automáticas han cubierto la lógica interna de un modelo y la forma en que nuestras vistas publican información, uno podría usar un framework “in-browser” como “Selenium” para probar la manera en que el HTML se “renderiza” en un navegador. Estas herramientas nos permiten no sólo comprobar el comportamiento de nuestro código Django, sino también, por ejemplo, nuestro JavaScript. Es algo muy curioso ver como las pruebas ejecutan un navegador y empiezan a interactuar con nuestro sitio como si un humano lo estuviera controlando. Django incluye “LiveServerTestCase” para facilitar la integración con herramientas como “Selenium”.

Si uno tiene una aplicación compleja, podría querer correr las pruebas automáticamente cada vez que vaya a guardar el código en un repositorio, para ir realizando un control de calidad.

Aquí lo dejamos por hoy, os invitamos como siempre a que sigáis explorando este framework y probando. En el próximo capítulo empezaremos a personalizar nuestra aplicación para que tenga un aspecto más atractivo.

Y para todos los que se acaban de incorporar indicarles que tenemos un índice con todos los capítulos del curso, ya que nunca es tarde para empezar.

Últimos análisis

Valoración RZ
7
Valoración RZ
9
Valoración RZ
8
Valoración RZ
8
Valoración RZ
8
Valoración RZ
8
Valoración RZ
10