# documentación de Django - 5.1.7

# Escribiendo su primera aplicación en Django, parte 5

Este tutorial comienza donde terminó el Tutorial 4. Hemos creado una aplicación web de encuestas y ahora crearemos algunas pruebas automatizadas para ella.

Dónde obtener ayuda:

Si tiene problemas para seguir este tutorial, diríjase a la sección Obteniendo ayuda del FAQ.

## Presentando las pruebas automatizadas

### ¿Qué son las pruebas automatizadas?

Las pruebas son rutinas que comprueban el funcionamiento de su código.

Las pruebas se ejecutan en diferentes niveles. Algunas pruebas se pueden aplicar a un pequeño detalle (¿Un método de modelo particular retorna valores como se espera?) mientras que otras examinan el total funcionamiento del software (¿Una secuencia de entradas de usuarios en el sitio produce el resultado deseado?). No se diferencia del tipo de pruebas que ejecutó anteriormente en el Tutorial 2 utilizando el comando `shell` para examinar el comportamiento de un método, o ejecutar la aplicación e ingresar datos para comprobar cómo se comporta.

Lo que es diferente en las pruebas automatizadas es que el trabajo de pruebas lo hace por usted el sistema. Usted crea un conjunto de pruebas una vez, y después, a medida que modifica su aplicación, puede comprobar que el código sigue funcionando como lo concibió en un principio, sin necesidad de engorrosas pruebas manuales.

### El por qué es necesario crear pruebas

Entonces, ¿por qué crear pruebas y por qué ahora?

Usted podría pensar que ya con aprender Python y Django tiene bastante con qué ocuparse, y que tener que aprender y hacer aún más cosas podría ser abrumador y hasta innecesario. Después de todo, nuestra aplicación encuestas está funcionando ahora bastante bien, tomarse la molestia de crear pruebas automatizadas no va a hacer que funcione mejor. Si crear la aplicación encuestas es el último fragmento de programa de Django que hará, entonces es verdad, usted no necesita saber cómo crear pruebas automatizadas. Sin embargo, si ese no es el caso, ahora es un momento excelente para aprender.

#### Las pruebas le ahorrarán tiempo

Hasta cierto punto, “comprobar de que parece funcionar” será una prueba satisfactoria. En una aplicación más sofisticada, es posible que tenga decenas de interacciones complejas entre los componentes.

Un cambio en cualquiera de esos componentes podría tener consecuencias inesperadas en el comportamiento de la aplicación. Comprobar que todavía «parece funcionar» podría significar ejecutar la funcionalidad de su código con veinte variaciones diferentes de sus datos de prueba para asegurarse de que no se haya roto algo, lo que supone un desperdicio de su tiempo.

Eso es especialmente cierto cuando las pruebas automatizadas pueden hacer esto por usted en cuestión de segundos. Si algo salió mal, las pruebas también ayudarán a identificar el código que está causando el comportamiento inesperado.

A veces puede parecer un fastidio apartarse de su trabajo creativo y productivo de programación para afrontar la obligación poca atractiva y aburrida de escribir pruebas, particularmente cuando sabe que su código funciona de forma adecuada.

Sin embargo, la tarea de escribir pruebas es mucho más gratificante que pasar horas probando su aplicación de forma manual o intentando identificar la causa de un problema recién introducido.

#### Las pruebas no solo identifican los problemas, los previenen

Es un error pensar en las pruebas sólo como un aspecto negativo del desarrollo.

Sin las pruebas, el propósito o el comportamiento previsto de una aplicación podría ser bastante incomprensible. Aún cuando es su propio código, usted a veces se encontrará buscando en él tratando de averiguar qué es exactamente lo que hace.

Las pruebas cambian eso; iluminan su código desde adentro, y cuando algo sale mal, ellas se centran en la parte que salió mal; incluso si usted no se ha dado cuenta de que salió mal.

#### Las pruebas hacen más atractivo su código

Es posible que haya creado un software brillante, pero descubrirá que muchos desarrolladores se negarán a verlo porque carece de pruebas; sin pruebas, no van a confiar en él. Jacob Kaplan-Moss, uno de los desarrolladores originales de Django, dice: «El código sin pruebas es defectuoso por diseño».

Que otros desarrolladores quieran ver pruebas en su software antes de que se lo tomen en serio es una razón más para que usted comience a escribir las pruebas.

#### Las pruebas ayudan a los equipos a trabajar en conjunto

Los puntos anteriores están escritos desde el punto de vista de un único desarrollador que mantiene una aplicación. Las aplicaciones complejas serán mantenidas por equipos. Las pruebas garantizan que los colegas no rompan inadvertidamente su código (y que usted no rompa el de ellos sin saber). Si quiere ganarse la vida como un programador de Django, ¡Usted debe ser bueno escribiendo pruebas!

## Estrategias básicas de pruebas

Hay muchas maneras de abordar la escritura de pruebas.

Some programmers follow a discipline called «test-driven development»; they actually write their tests before they write their code. This might seem counterintuitive, but in fact it’s similar to what most people will often do anyway: they describe a problem, then create some code to solve it. Test-driven development formalizes the problem in a Python test case.

La mayoría de las veces, un principiante en pruebas va a crear código y luego decidirá que este debe tener algunas pruebas. Tal vez hubiera sido mejor escribir algunas pruebas antes, pero nunca es demasiado tarde para empezar.

A veces es difícil saber por dónde empezar con la escritura de pruebas. Si usted ha escrito varias miles de líneas de Python, elegir algo para probar podría no ser fácil. En tal caso, es provechoso escribir su primera prueba la próxima vez que realice una modificación, ya sea cuando agregue una nueva funcionalidad o solucione un error.

Así que vamos a hacer eso de inmediato.

## Escribiendo nuestra primera prueba

### Identificamos un error

Afortunadamente, hay un pequeño error en la aplicación `polls` para que lo solucionemos de inmediato: el método `Question.was_published_recently()` retorna `True` si la `Question` fue publicada en el último día (que es correcto), pero también si el campo de `pub_date` de `Question` está en el futuro (que de hecho no lo está).

Confirme el error usando el `shell` para comprobar el método en una pregunta cuya fecha sea futura:

/ 

    $ python manage.py shell

    ...\> py manage.py shell

    >>> import datetime
    >>> from django.utils import timezone
    >>> from polls.models import Question
    >>> # create a Question instance with pub_date 30 days in the future
    >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
    >>> # was it published recently?
    >>> future_question.was_published_recently()
    True

Puesto que las cosas en el futuro no son “recientes”, esto es claramente erróneo.

### Cree una prueba para exponer el error

Lo que acabamos de hacer en el comando `shell` para detectar el problema es exactamente lo que podemos hacer en una prueba automatizada, así que vamos a convertir eso en una prueba automatizada.

Un lugar convencional para las pruebas de una aplicación se encuentra en el archivo `tests.py` de la aplicación; el sistema de pruebas encontrará automáticamente las pruebas en cualquier archivo cuyo nombre comience con `test`.

Ponga lo siguiente en el archivo `tests.py` de la aplicación `polls`:

`polls/tests.py`

    import datetime

    from django.test import TestCase
    from django.utils import timezone

    from .models import Question

    class QuestionModelTests(TestCase):
        def test_was_published_recently_with_future_question(self):
            """
            was_published_recently() returns False for questions whose pub_date
            is in the future.
            """
            time = timezone.now() + datetime.timedelta(days=30)
            future_question = Question(pub_date=time)
            self.assertIs(future_question.was_published_recently(), False)

Here we have created a `django.test.TestCase` subclass with a method that creates a `Question` instance with a `pub_date` in the future. We then check the output of `was_published_recently()` - which ought to be False.

### Ejecutando las pruebas

In the terminal, we can run our test:

/ 

    $ python manage.py test polls

    ...\> py manage.py test polls

and you’ll see something like:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    F
    ======================================================================
    FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/path/to/djangotutorial/polls/tests.py", line 16, in test_was_published_recently_with_future_question
        self.assertIs(future_question.was_published_recently(), False)
    AssertionError: True is not False

    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    FAILED (failures=1)
    Destroying test database for alias 'default'...

Different error?

If instead you’re getting a `NameError` here, you may have missed a step in Part 2 where we added imports of `datetime` and `timezone` to `polls/models.py`. Copy the imports from that section, and try running your tests again.

Esto es lo que ocurrió:

- `manage.py test polls` looked for tests in the `polls` application

- encontró una subclase de la clase `django.test.TestCase`

- creó una base de datos especial para las pruebas

- buscó métodos de prueba; aquellos cuyos nombres comienzan con `test`

- en `test_was_published_recently_with_future_question` creó una instancia `Question` cuyo campo de `pub_date` está 30 días en el futuro

- … y utilizando el método `assertIs()` descubrió que `was_published_recently()` retorna `True`, aunque queríamos que retornara `False`

La prueba nos informa que prueba falló e incluso la línea en la que se produjo el error.

### Solucionando el error

Ya sabemos cuál es el problema: `Question.was_published_recently()` debe devolver `False` si su `pub_date` está en el futuro. Modifique el método `models.py`, de modo que este sólo retornará `True` si la fecha está también en el pasado:

`polls/models.py`

    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

and run the test again:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    OK
    Destroying test database for alias 'default'...

Después de identificar un error, escribimos una prueba que lo expone y corregimos el error en el código para que nuestra prueba pase.

Many other things might go wrong with our application in the future, but we can be sure that we won’t inadvertently reintroduce this bug, because running the test will warn us immediately. We can consider this little portion of the application pinned down safely forever.

### Pruebas más exhaustivas

Ya que estamos aquí, podemos además precisar el método `was_published_recently()`; de hecho, sería verdaderamente vergonzoso si al corregir un error hubiéramos introducido otro.

Agregue dos métodos de pruebas más a la misma clase, para probar el comportamiento del método de forma más completa:

`polls/tests.py`

    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)

    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() returns True for questions whose pub_date
        is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)
        self.assertIs(recent_question.was_published_recently(), True)

Y ahora tenemos tres pruebas que confirman que `Question.was_published_recently()` retorna valores razonables para preguntas pasadas, recientes y futuras.

Again, `polls` is a minimal application, but however complex it grows in the future and whatever other code it interacts with, we now have some guarantee that the method we have written tests for will behave in expected ways.

## Pruebe una vista

La aplicación encuestas no discrimina en lo absoluto: publicará cualquier pregunta, incluyendo aquellas cuyo campo de `pub_date` esté en el futuro. Deberíamos actualizar esto. Establecer un `pub_date` en el futuro debería significar que la pregunta se publica en ese momento, pero que será invisible hasta entonces.

### Una prueba para una vista

When we fixed the bug above, we wrote the test first and then the code to fix it. In fact that was an example of test-driven development, but it doesn’t really matter in which order we do the work.

En nuestra primera prueba, nos centramos detalladamente en el comportamiento interno del código. Para esta prueba, queremos comprobar su comportamiento como lo experimentaría un usuario a través de un navegador web.

Antes de tratar de corregir algo, echemos un vistazo a las herramientas a nuestra disposición.

### El cliente de prueba de Django

Django proporciona una prueba `Client` para simular un usuario interactúando con el código al nivel de la vista. Podemos utilizarlo en `tests.py` o incluso en el `shell`.

We will start again with the `shell`, where we need to do a couple of things that won’t be necessary in `tests.py`. The first is to set up the test environment in the `shell`:

/ 

    $ python manage.py shell

    ...\> py manage.py shell

    >>> from django.test.utils import setup_test_environment
    >>> setup_test_environment()

`setup_test_environment()` installs a template renderer which will allow us to examine some additional attributes on responses such as `response.context` that otherwise wouldn’t be available. Note that this method does not set up a test database, so the following will be run against the existing database and the output may differ slightly depending on what questions you already created. You might get unexpected results if your `TIME_ZONE` in `settings.py` isn’t correct. If you don’t remember setting it earlier, check it before continuing.

Next we need to import the test client class (later in `tests.py` we will use the `django.test.TestCase` class, which comes with its own client, so this won’t be required):

    >>> from django.test import Client
    >>> # create an instance of the client for our use
    >>> client = Client()

With that ready, we can ask the client to do some work for us:

    >>> # get a response from '/'
    >>> response = client.get("/")
    Not Found: /
    >>> # we should expect a 404 from that address; if you instead see an
    >>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
    >>> # omitted the setup_test_environment() call described earlier.
    >>> response.status_code
    404
    >>> # on the other hand we should expect to find something at '/polls/'
    >>> # we'll use 'reverse()' rather than a hardcoded URL
    >>> from django.urls import reverse
    >>> response = client.get(reverse("polls:index"))
    >>> response.status_code
    200
    >>> response.content
    b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
    >>> response.context["latest_question_list"]
    <QuerySet [<Question: What's up?>]>

### Mejorando nuestra vista

La lista de encuestas muestra las encuestas que aún no están publicadas (es decir, aquellas que tienen una `pub_date` en el futuro). Vamos a solucionar eso.

En el Tutorial 4 presentamos una vista basada en clases, basada en `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("-pub_date")[:5]

Tenemos que modificar el método `get_queryset()` y cambiarlo para que también compruebe la fecha comparándolo con `timezone.now()`. En primer lugar tenemos que añadir un import:

`polls/views.py`

    from django.utils import timezone

y luego hay que modificar el método `get_queryset` como sigue:

`polls/views.py`

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

`Question.objects.filter(pub_date__lte=timezone.now())` returns a queryset containing `Question`s whose `pub_date` is less than or equal to - that is, earlier than or equal to - `timezone.now()`.

### Probando nuestra nueva vista

Now you can satisfy yourself that this behaves as expected by firing up `runserver`, loading the site in your browser, creating `Questions` with dates in the past and future, and checking that only those that have been published are listed. You don’t want to have to do that every single time you make any change that might affect this - so let’s also create a test, based on our `shell` session above.

Agregue lo siguiente a `polls/tests.py`:

`polls/tests.py`

    from django.urls import reverse

y vamos a crear una función de atajo para crear preguntas, así como una nueva clase de pruebas:

`polls/tests.py`

    def create_question(question_text, days):
        """
        Create a question with the given `question_text` and 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(question_text=question_text, pub_date=time)

    class QuestionIndexViewTests(TestCase):
        def test_no_questions(self):
            """
            If no questions exist, an appropriate message is 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_past_question(self):
            """
            Questions with a pub_date in the past are displayed on the
            index page.
            """
            question = create_question(question_text="Past question.", days=-30)
            response = self.client.get(reverse("polls:index"))
            self.assertQuerySetEqual(
                response.context["latest_question_list"],
                [question],
            )

        def test_future_question(self):
            """
            Questions with a pub_date in the future aren't displayed on
            the index page.
            """
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse("polls:index"))
            self.assertContains(response, "No polls are available.")
            self.assertQuerySetEqual(response.context["latest_question_list"], [])

        def test_future_question_and_past_question(self):
            """
            Even if both past and future questions exist, only past questions
            are displayed.
            """
            question = create_question(question_text="Past question.", days=-30)
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse("polls:index"))
            self.assertQuerySetEqual(
                response.context["latest_question_list"],
                [question],
            )

        def test_two_past_questions(self):
            """
            The questions index page may display multiple questions.
            """
            question1 = create_question(question_text="Past question 1.", days=-30)
            question2 = create_question(question_text="Past question 2.", days=-5)
            response = self.client.get(reverse("polls:index"))
            self.assertQuerySetEqual(
                response.context["latest_question_list"],
                [question2, question1],
            )

Veamos algunas de estas más de cerca.

En primer lugar es una función de atajo a las preguntas, `create_question`, para eliminar parte de la repetición del proceso de creación de preguntas.

`test_no_questions` doesn’t create any questions, but checks the message: «No polls are available.» and verifies the `latest_question_list` is empty. Note that the `django.test.TestCase` class provides some additional assertion methods. In these examples, we use `assertContains()` and `assertQuerySetEqual()`.

En `test_past_question` creamos una pregunta y verificamos que aparezca en la lista.

En `test_future_question` creamos una pregunta con una `pub_date` en el futuro. La base de datos se reinicia para cada método de prueba, por lo que la primera pregunta ya no está allí, de modo que de nuevo el índice no debería tener ninguna pregunta.

Y así sucesivamente. En efecto, estamos utilizando las pruebas para contar una historia del ingreso de datos en el sitio administrativo y experiencia de usuario en el sitio, y comprobar que los resultados esperados se publican en cada estado y para cada nuevo cambio en el estado del sistema.

### Probando la `DetailView`

Lo que tenemos funciona bien; sin embargo, a pesar de que las preguntas futuras no aparecen en el índice, los usuarios pueden todavía llegar a ellas si saben o adivinan la dirección URL correcta. Así que tenemos que agregar una restricción similar a la `DetailView`:

`polls/views.py`

    class DetailView(generic.DetailView):
        ...

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

We should then add some tests, to check that a `Question` whose `pub_date` is in the past can be displayed, and that one with a `pub_date` in the future is not:

`polls/tests.py`

    class QuestionDetailViewTests(TestCase):
        def test_future_question(self):
            """
            The detail view of a question with a pub_date in the future
            returns a 404 not found.
            """
            future_question = create_question(question_text="Future question.", days=5)
            url = reverse("polls:detail", args=(future_question.id,))
            response = self.client.get(url)
            self.assertEqual(response.status_code, 404)

        def test_past_question(self):
            """
            The detail view of a question with a pub_date in the past
            displays the question's text.
            """
            past_question = create_question(question_text="Past Question.", days=-5)
            url = reverse("polls:detail", args=(past_question.id,))
            response = self.client.get(url)
            self.assertContains(response, past_question.question_text)

### Ideas para más pruebas

Debemos agregar un método `get_queryset` similar a `ResultsView` y crear una nueva clase de pruebas para esa vista. Va a ser muy similar a lo que acabamos de crear; de hecho, habrá mucha repetición.

También podríamos mejorar nuestra aplicación de otras maneras, agregando pruebas a lo largo del camino. Por ejemplo, es una tontería que se puedan publicar `Questions` en el sitio que no tienen `Choices`. Por lo tanto, nuestras vistas podrían comprobar esto y excluir dichas `Questions`. Nuestras pruebas crearían una `Question` sin `Choices` y luego probarían que no está publicada, así como crearían una `Question` similar con `Choices` y probarían que está publicada.

Quizás se les debería permitir a los usuarios registrados en el sitio administrativo ver las `Questions` que no han sido publicadas, pero no a los visitantes ordinarios. Una vez más: lo que se tenga que agregar al software para lograr esto debería ir acompañado de una prueba, ya sea que escriba la prueba primero y luego haga que el código pase la prueba, o primero solucione la lógica en el código y luego escriba una prueba para probarlo.

En un momento dado usted está obligado a examinar sus pruebas y a preguntarse si su código sufre de exceso de pruebas, lo que nos lleva a:

## Cuando se trata de pruebas, más es mejor

Podría parecer que nuestras pruebas están creciendo fuera de control. A este paso pronto habrá más código en nuestras pruebas que en nuestra aplicación y la repetición es poco estética en comparación con la refinada concisión del resto de nuestro código.

No importa, deje que crezcan. Por lo general, usted puede escribir una prueba una vez y luego olvidarse de ella. Esta seguirá cumpliendo su función útil mientras usted continúe desarrollando su programa.

A veces las pruebas tendrán que ser actualizadas. Supongamos que modificamos nuestras vistas de modo que sólo las `Questions` con `Choices` se publiquen. En ese caso, muchas de nuestras pruebas existentes fallarán, indicándonos exactamente qué pruebas necesitan ser modificadas para actualizarlas, de modo que hasta ese punto las pruebas ayudan a ocuparse de sí mismas.

En el peor de los casos, a medida que continúe desarrollando, usted podría encontrar que tiene algunas pruebas que ahora son redundantes. Incluso eso no es un problema, cuando se trata de pruebas, la redundancia es algo bueno.

Siempre y cuando las pruebas se organicen de forma razonable, no van a ser difíciles de manejar. Las reglas básicas de uso incluyen el hecho de tener:

- una `TestClass` independiente para cada modelo o vista

- un método de prueba independiente para cada conjunto de condiciones que usted quiere probar

- nombres de los métodos de prueba que describan su función

## Pruebas adicionales

Este tutorial sólo presenta algunos de los temas fundamentales de las pruebas. Hay mucho más que usted puede hacer y una serie de herramientas muy útiles a su disposición para lograr algunas cosas muy interesantes.

Por ejemplo, aunque nuestras pruebas han tratado aquí algo de la lógica interna de un modelo y la forma en que nuestras vistas publican información, usted puede utilizar un framework «en el navegador» como Selenium para probar la forma en que su HTML en realidad se renderiza en un navegador. Estas herramientas le permiten comprobar no sólo el comportamiento de su código Django, sino también, por ejemplo, el de su JavaScript. ¡Es impresionante ver cómo las pruebas ejecutan un navegador y comienzan a interactuar con su sitio como si estuviese siendo operado por un humano! Django incluye `LiveServerTestCase` para facilitar la integración con herramientas como Selenium.

Si usted tiene una aplicación compleja, es posible que desee ejecutar las pruebas de forma automática con cada commit a efectos de la integración continua_, por lo que el control de calidad está en sí, al menos parcialmente, automatizado.

Una buena manera de detectar las partes de su aplicación que no han sido probadas es comprobar la cobertura de código. Esto también ayuda a identificar el código frágil o incluso muerto. Si no puede probar un trozo de código, por lo general significa que el código debe ser reestructurado o eliminado. La cobertura ayudará a identificar el código muerto. Consulte Integration with coverage.py para más detalles.

Las pruebas en Django tiene información completa acerca de las pruebas.

## ¿Qué sigue?

Para más detalles sobre las pruebas, consulte Las pruebas en Django.

Cuando esté familiarizado con las pruebas de las vistas de Django, lea la parte 6 de este tutorial para aprender sobre la gestión de los archivos estáticos.