Crear algunas pruebas automatizadas

DjangoDjangoBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

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.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL django(("Django")) -.-> django/CoreConfigurationandRoutingGroup(["Core Configuration and Routing"]) django(("Django")) -.-> django/UserInterfaceandInteractionGroup(["User Interface and Interaction"]) django/CoreConfigurationandRoutingGroup -.-> django/django_urls("Django Urls") django/UserInterfaceandInteractionGroup -.-> django/built_in_views("Built-in Views") subgraph Lab Skills django/django_urls -.-> lab-153745{{"Crear algunas pruebas automatizadas"}} django/built_in_views -.-> lab-153745{{"Crear algunas pruebas automatizadas"}} end

Presentación de las pruebas automatizadas

¿Qué son las pruebas automatizadas?

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.

Por qué es necesario crear pruebas

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.

Las pruebas ahorrarán tiempo

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.

Las pruebas no solo identifican problemas, sino que los previenen

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.

Las pruebas hacen que su código sea más atractivo

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.

Las pruebas ayudan a los equipos a trabajar juntos

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.

Estrategias básicas de prueba

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.

Escribiendo nuestra primera prueba

Identificamos un error

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.

Creamos una prueba para detectar el error

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.

Ejecutando las pruebas

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
  • encontró una subclase de la clase django.test.TestCase
  • creó una base de datos especial con el propósito de probar
  • buscó métodos de prueba, aquellos cuyo nombre comienza con test
  • en test_was_published_recently_with_future_question creó una instancia de Question cuyo campo pub_date está 30 días en el futuro
    -... y usando el método assertIs(), 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.

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

Pruebas más exhaustivas

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.

Probar una vista

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.

Una prueba para una vista

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.

El cliente de prueba de Django

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&#x27;s up?</a></li>\n    \n    </ul>\n\n'
>>> response.context["latest_question_list"]
<QuerySet [<Question: What's up?>]>

Mejora de nuestra vista

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.

Prueba de nuestra nueva vista

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.

Prueba de la 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)

Ideas para más pruebas

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 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:

Cuando se realizan pruebas, más es mejor

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:

  • una TestClass separada para cada modelo o vista
  • un método de prueba separado para cada conjunto de condiciones que desee probar
  • nombres de métodos de prueba que describan su función

Pruebas adicionales

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

Resumen

¡Felicidades! Has completado el laboratorio de Creación de Pruebas Automatizadas. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.