Introducción
Este tutorial comienza donde dejó **Procesamiento de formularios y reducción de nuestro código**
. Hemos construido una aplicación de sondeo web y ahora crearemos algunas pruebas automatizadas para ella.
💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí
Este tutorial comienza donde dejó **Procesamiento de formularios y reducción de nuestro código**
. Hemos construido una aplicación de sondeo web y ahora crearemos algunas pruebas automatizadas para ella.
Las pruebas son rutinas que verifican el funcionamiento de su código.
Las pruebas se realizan a diferentes niveles. Algunas pruebas pueden aplicarse a un detalle minúsculo (¿devuelve un método de modelo particular los valores esperados?), mientras que otras examinan el funcionamiento general del software (¿produce una secuencia de entradas de usuario en el sitio el resultado deseado?). Eso no es diferente al tipo de pruebas que realizó anteriormente en **Configurar la base de datos**
, usando la shell
para examinar el comportamiento de un método, o ejecutando la aplicación y entrando datos para comprobar cómo se comporta.
Lo que es diferente en las pruebas automatizadas es que el trabajo de prueba se realiza por el sistema. Crea un conjunto de pruebas una vez, y luego, a medida que realiza cambios en su aplicación, puede comprobar que su código sigue funcionando como originalmente planeó, sin tener que realizar pruebas manuales que consumen mucho tiempo.
Entonces, ¿por qué crear pruebas y por qué ahora?
Es posible que sienta que tiene bastante en su plato solo aprendiendo Python/Django, y que tener otra cosa que aprender y hacer puede parecer abrumador y quizás innecesario. Después de todo, nuestra aplicación de sondeos está funcionando muy bien ahora; pasar por el trabajo de crear pruebas automatizadas no la hará funcionar mejor. Si crear la aplicación de sondeos es la última parte de programación de Django que hará, entonces, es cierto, no necesita saber cómo crear pruebas automatizadas. Pero, si no es el caso, ahora es un excelente momento para aprender.
Hasta cierto punto, "verificar que parece funcionar" será una prueba satisfactoria. En una aplicación más sofisticada, es posible que haya docenas de interacciones complejas entre componentes.
Un cambio en cualquiera de esos componentes podría tener consecuencias inesperadas en el comportamiento de la aplicación. Verificar que todavía "parece funcionar" podría significar probar la funcionalidad de su código con veinte variaciones diferentes de sus datos de prueba para asegurarse de que no ha roto nada, lo que no es una buena utilización de su tiempo.
Eso es especialmente cierto cuando las pruebas automatizadas podrían hacer esto por usted en segundos. Si algo ha salido mal, las pruebas también ayudarán a identificar el código que está causando el comportamiento inesperado.
A veces puede parecer una tarea molesta apartarse de su trabajo productivo y creativo de programación para enfrentarse al trabajo sin glamour y aburrido de escribir pruebas, especialmente cuando sabe que su código está funcionando correctamente.
Sin embargo, la tarea de escribir pruebas es mucho más gratificante que pasar horas probando manualmente su aplicación o tratar de identificar la causa de un problema recientemente introducido.
Es un error pensar en las pruebas solo como un aspecto negativo del desarrollo.
Sin pruebas, el propósito o el comportamiento previsto de una aplicación puede ser bastante opaco. Incluso cuando es su propio código, a veces se encontrará buscando en él tratando de averiguar exactamente lo que está haciendo.
Las pruebas cambian eso; iluminan su código desde dentro, y cuando algo sale mal, enfocan la luz en la parte que ha fallado, incluso si no se había dado cuenta de que había salido mal.
Es posible que haya creado un excelente software, pero encontrará que muchos otros desarrolladores se negarán a mirarlo porque carece de pruebas; sin pruebas, no lo confiarán. Jacob Kaplan-Moss, uno de los primeros desarrolladores de Django, dice: "El código sin pruebas está roto por diseño".
Que otros desarrolladores quieran ver pruebas en su software antes de tomarlo en serio es otra razón para que empiece a escribir pruebas.
Los puntos anteriores se escriben desde el punto de vista de un solo 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 lo rompa sin saber). Si desea ganarse la vida como programador de Django, debe ser bueno escribiendo pruebas.
Hay muchas maneras de abordar la escritura de pruebas.
Algunos programadores siguen una disciplina llamada "desarrollo dirigido por pruebas"; en realidad, escriben sus pruebas antes de escribir su código. Esto puede parecer contraintuitivo, pero de hecho es similar a lo que la mayoría de las personas harán a menudo de todos modos: describen un problema y luego crean algún código para resolverlo. El desarrollo dirigido por pruebas formaliza el problema en un caso de prueba de Python.
Con más frecuencia, un recién llegado al mundo de las pruebas creará algún código y luego decidirá que debería tener algunas pruebas. Tal vez hubiera sido mejor escribir algunas pruebas antes, pero nunca es demasiado tarde para comenzar.
A veces es difícil saber por dónde empezar a escribir pruebas. Si ha escrito varios miles de líneas de Python, elegir algo para probar puede no ser fácil. En ese caso, es fructífero escribir su primera prueba la próxima vez que realice un cambio, ya sea cuando agregue una nueva característica o corrija un error.
Así que hagámoslo inmediatamente.
Afortunadamente, hay un pequeño error en la aplicación polls
que podemos corregir inmediatamente: el método Question.was_published_recently()
devuelve True
si la Question
se publicó dentro del último día (lo cual es correcto), pero también si el campo pub_date
de la Question
está en el futuro (lo cual ciertamente no es).
Confirme el error usando la shell
para comprobar el método en una pregunta cuya fecha está en el futuro:
cd ~/project/mysite
python manage.py shell
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> ## crea una instancia de Question con pub_date 30 días en el futuro
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> ## ¿se publicó recientemente?
>>> future_question.was_published_recently()
True
Como las cosas en el futuro no son'recientes', esto está claramente mal.
Lo que acabamos de hacer en la shell
para probar el problema es exactamente lo que podemos hacer en una prueba automatizada, así que convertámoslo en una prueba automatizada.
Un lugar convencional para las pruebas de una aplicación es en el archivo tests.py
de la aplicación; el sistema de prueba buscará automáticamente las pruebas en cualquier archivo cuyo nombre comience con test
.
Coloque lo siguiente en el archivo tests.py
de la aplicación polls
:
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() devuelve False para preguntas cuya pub_date
está en el futuro.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
Aquí hemos creado una subclase de django.test.TestCase
con un método que crea una instancia de Question
con un pub_date
en el futuro. Luego comprobamos la salida de was_published_recently()
- que debería ser False.
En la terminal, podemos ejecutar nuestra prueba:
python manage.py test polls
y verá algo como:
[object Object]
¿Error diferente?
Si en su lugar está recibiendo un NameError
aquí, es posible que haya omitido un paso en la Parte 2 <tutorial02-import-timezone>
donde agregamos las importaciones de datetime
y timezone
a polls/models.py
. Copie las importaciones de esa sección y vuelva a intentar ejecutar sus pruebas.
Lo que pasó es lo siguiente:
manage.py test polls
buscó pruebas en la aplicación polls
django.test.TestCase
test
test_was_published_recently_with_future_question
creó una instancia de Question
cuyo campo pub_date
está 30 días en el futuroassertIs()
, descubrió que su was_published_recently()
devuelve True
, aunque queríamos que devolviera False
La prueba nos informa cuál prueba falló e incluso la línea en la que ocurrió el error.
Ya sabemos cuál es el problema: Question.was_published_recently()
debería devolver False
si su pub_date
está en el futuro. Amende el método en models.py
para que solo devuelva True
si la fecha también está en el pasado:
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
y ejecute la prueba nuevamente:
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.
Muchas otras cosas pueden salir mal con nuestra aplicación en el futuro, pero podemos estar seguros de que no volveremos a introducir inadvertidamente este error, porque ejecutar la prueba nos advertirá inmediatamente. Podemos considerar esta pequeña parte de la aplicación fijada de manera segura para siempre.
Mientras estamos aquí, podemos fijar aún más el método was_published_recently()
; de hecho, sería vergonzoso si al corregir un error hubiéramos introducido otro.
Agregue dos métodos de prueba más a la misma clase para probar el comportamiento del método de manera más exhaustiva:
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() devuelve False para preguntas cuya pub_date
es anterior a 1 día.
"""
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() devuelve True para preguntas cuya pub_date
está dentro del último día.
"""
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()
devuelve valores sensatos para preguntas pasadas, recientes y futuras.
Nuevamente, polls
es una aplicación mínima, pero sin importar cuán compleja crezca en el futuro y con cualquier otro código con el que interactúe, ahora tenemos alguna garantía de que el método para el que escribimos pruebas se comportará de manera esperada.
La aplicación de sondeos es bastante permissiva: publicará cualquier pregunta, incluyendo aquellas cuyo campo pub_date
está en el futuro. Debemos mejorar esto. Establecer un pub_date
en el futuro debería significar que la Pregunta se publique en ese momento, pero sea invisible hasta entonces.
Cuando corregimos el error anterior, escribimos la prueba primero y luego el código para corregirla. De hecho, eso fue un ejemplo de desarrollo dirigido por pruebas, pero en realidad no importa en qué orden hagamos el trabajo.
En nuestra primera prueba, nos centramos en el comportamiento interno del código. Para esta prueba, queremos comprobar su comportamiento tal como lo experimentaría un usuario a través de un navegador web.
Antes de intentar corregir algo, echemos un vistazo a las herramientas a nuestro alcance.
Django proporciona un cliente de prueba ~django.test.Client
para simular la interacción de un usuario con el código en el nivel de vista. Lo podemos usar en tests.py
o incluso en la shell
.
Vamos a comenzar de nuevo con la shell
, donde necesitamos hacer un par de cosas que no serán necesarias en tests.py
. La primera es configurar el entorno de prueba en la shell
:
python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
~django.test.utils.setup_test_environment
instala un renderizador de plantillas que nos permitirá examinar algunos atributos adicionales en las respuestas, como response.context
, que de lo contrario no estarían disponibles. Tenga en cuenta que este método no configura una base de datos de prueba, por lo que lo siguiente se ejecutará contra la base de datos existente y la salida puede variar ligeramente dependiendo de las preguntas que ya haya creado. Es posible que obtenga resultados inesperados si su TIME_ZONE
en settings.py
no es correcta. Si no recuerda haberla configurado anteriormente, compruébela antes de continuar.
A continuación, necesitamos importar la clase del cliente de prueba (más adelante en tests.py
usaremos la clase django.test.TestCase
, que viene con su propio cliente, por lo que esto no será necesario):
>>> from django.test import Client
>>> ## crea una instancia del cliente para nuestro uso
>>> client = Client()
Con eso listo, podemos pedirle al cliente que haga algunos trabajos para nosotros:
>>> ## obtenga una respuesta de '/'
>>> response = client.get("/")
Not Found: /
>>> ## deberíamos esperar un 404 de esa dirección; si en su lugar ve un
>>> ## error "Invalid HTTP_HOST header" y una respuesta 400, es probable que
>>> ## haya omitido la llamada a setup_test_environment() descrita anteriormente.
>>> response.status_code
404
>>> ## por otro lado, deberíamos esperar encontrar algo en '/polls/'
>>> ## usaremos'reverse()' en lugar de una URL codificada en duro
>>> 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's up?</a></li>\n \n </ul>\n\n'
>>> response.context["latest_question_list"]
<QuerySet [<Question: What's up?>]>
La lista de sondeos muestra sondeos que aún no se han publicado (es decir, aquellos que tienen un pub_date
en el futuro). Vamos a corregir eso.
En **Procesamiento de formularios y reducción de nuestro código**
introdujimos una vista basada en clases, basada en ~django.views.generic.list.ListView
:
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""Devuelve las últimas cinco preguntas publicadas."""
return Question.objects.order_by("-pub_date")[:5]
Debemos corregir el método get_queryset()
y cambiarlo para que también verifique la fecha comparándola con timezone.now()
. Primero debemos agregar una importación:
from django.utils import timezone
y luego debemos corregir el método get_queryset
de la siguiente manera:
def get_queryset(self):
"""
Devuelve las últimas cinco preguntas publicadas (sin incluir aquellas
establecidas para ser publicadas en el futuro).
"""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[
:5
]
Question.objects.filter(pub_date__lte=timezone.now())
devuelve un conjunto de consultas que contiene Question
cuya pub_date
es menor o igual a, es decir, anterior o igual a, timezone.now
.
Ahora puede comprobar que esto se comporta como se espera encendiendo runserver
, cargando el sitio en su navegador, creando Questions
con fechas en el pasado y el futuro y comprobando que solo se muestran aquellas que se han publicado. No quiere tener que hacer eso cada vez que realice cualquier cambio que pueda afectar esto, así que también creemos una prueba, basada en nuestra sesión de shell
anterior.
Agregue lo siguiente a polls/tests.py
:
from django.urls import reverse
y crearemos una función atajo para crear preguntas así como una nueva clase de prueba:
def create_question(question_text, days):
"""
Crea una pregunta con el `question_text` dado y publicada el
número dado de `días` desplazado hacia ahora (negativo para preguntas
publicadas en el pasado, positivo para preguntas que aún no se han
publicado).
"""
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):
"""
Si no existen preguntas, se muestra un mensaje adecuado.
"""
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):
"""
Las preguntas con un pub_date en el pasado se muestran en la
página de índice.
"""
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):
"""
Las preguntas con un pub_date en el futuro no se muestran en
la página de índice.
"""
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):
"""
Incluso si existen preguntas pasadas y futuras, solo se muestran
las preguntas pasadas.
"""
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):
"""
La página de índice de preguntas puede mostrar múltiples preguntas.
"""
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],
)
Echemos un vistazo más detenido a algunos de estos.
Primero está la función atajo de preguntas, create_question
, para evitar repetir el proceso de creación de preguntas.
test_no_questions
no crea ninguna pregunta, pero comprueba el mensaje: "No polls are available." y verifica que latest_question_list
esté vacío. Tenga en cuenta que la clase django.test.TestCase
proporciona algunos métodos de aserción adicionales. En estos ejemplos, usamos ~django.test.SimpleTestCase.assertContains()
y ~django.test.TransactionTestCase.assertQuerySetEqual()
.
En test_past_question
, creamos una pregunta y verificamos que aparezca en la lista.
En test_future_question
, creamos una pregunta con un 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í, y por lo tanto el índice no debería tener ninguna pregunta en él.
Y así sucesivamente. En efecto, estamos usando las pruebas para contar una historia de la entrada del administrador y la experiencia del usuario en el sitio y comprobando que en cada estado y para cada nuevo cambio en el estado del sistema, se publiquen los resultados esperados.
DetailView
Lo que tenemos funciona bien; sin embargo, aunque las preguntas futuras no aparecen en el índice, los usuarios aún pueden acceder a ellas si conocen o adivinan la URL correcta. Por lo tanto, necesitamos agregar una restricción similar a DetailView
:
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excluye cualquier pregunta que aún no se haya publicado.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
Luego deberíamos agregar algunas pruebas para comprobar que una Question
cuya pub_date
está en el pasado se puede mostrar y que una con un pub_date
en el futuro no lo está:
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
La vista detallada de una pregunta con un pub_date en el futuro
devuelve un 404 no encontrado.
"""
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):
"""
La vista detallada de una pregunta con un pub_date en el pasado
muestra el texto de la pregunta.
"""
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)
Deberíamos agregar un método get_queryset
similar a ResultsView
y crear una nueva clase de prueba para esa vista. 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 en el camino. Por ejemplo, es tonto que se puedan publicar Questions
en el sitio que no tienen Choices
. Entonces, nuestras vistas podrían comprobar esto y excluir tales Questions
. Nuestras pruebas crearían una Question
sin Choices
y luego probarían que no se publica, así como crear una Question
similar con Choices
y probar que sí se publica.
Tal vez los usuarios administradores autenticados deberían poder ver Questions
no publicadas, pero no los visitantes ordinarios. Nuevamente: todo lo que se agregue 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 que primero trabaje la lógica en su código y luego escriba una prueba para demostrarlo.
En cierto momento, seguro que mirará sus pruebas y se preguntará si su código está sufriendo de bloat de pruebas, lo que nos lleva a:
Podría parecer que nuestras pruebas están saliendo de control. A este ritmo, 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 elegante concisión del resto de nuestro código.
No importa. Déjales crecer. En su mayor parte, puede escribir una prueba una vez y luego olvidarse de ella. Continuará cumpliendo su función útil a medida que siga desarrollando su programa.
A veces, las pruebas necesitarán actualizarse. Supongamos que modificamos nuestras vistas para que solo se publiquen Questions
con Choices
. En ese caso, muchas de nuestras pruebas existentes fallarán, indicándonos exactamente qué pruebas deben modificarse para actualizarlas, por lo que en ese sentido las pruebas ayudan a autoatenderse.
En el peor de los casos, a medida que siga desarrollando, puede que encuentre que tiene algunas pruebas que ahora son redundantes. Incluso eso no es un problema; en pruebas, la redundancia es una buena cosa.
Mientras sus pruebas estén adecuadamente organizadas, no se volverán inservibles. Buenas reglas generales incluyen tener:
TestClass
separada para cada modelo o vistaEste tutorial solo introduce algunos de los conceptos básicos de las pruebas. Hay mucho más que se puede hacer, y hay una serie de herramientas muy útiles a su disposición para lograr algunas cosas muy inteligentes.
Por ejemplo, mientras nuestras pruebas aquí han cubierto algo de la lógica interna de un modelo y la forma en que nuestras vistas publican información, puede usar un marco "en el navegador" como Selenium para probar la forma en que su HTML se renderiza realmente en un navegador. Estas herramientas le permiten comprobar no solo el comportamiento de su código de Django, sino también, por ejemplo, de su JavaScript. Es bastante impresionante ver cómo las pruebas inician un navegador y comienzan a interactuar con su sitio, como si una persona lo estuviera manejando. Django incluye ~django.test.LiveServerTestCase
para facilitar la integración con herramientas como Selenium.
Si tiene una aplicación compleja, es posible que desee ejecutar las pruebas automáticamente con cada confirmación con fines de integración continua, de modo que el control de calidad sea, al menos en parte, automatizado.
Una buena manera de detectar las partes no probadas de su aplicación es comprobar la cobertura del código. Esto también ayuda a identificar código frágil o incluso código muerto. Si no puede probar un fragmento de código, generalmente significa que ese código debe refactorizarse o eliminarse. La cobertura ayudará a identificar el código muerto. Consulte temas-pruebas-cobertura-del-código
para obtener detalles.
Pruebas en Django </temas/pruebas/index>
tiene información detallada sobre las pruebas.
¡Felicidades! Has completado el laboratorio de Creación de Pruebas Automatizadas. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.