Introducción a las pruebas de código con PyTest
Una suave introducción a las pruebas con PyTest

Una prueba es código que ejecuta código. Cuando empiezas a desarrollar una nueva característica para tu proyecto Python, podrías formalizar sus requisitos como código. Cuando lo haces, no sólo documentas la forma en que se utilizará el código de tu implementación, sino que también puedes ejecutar todas las pruebas automáticamente para asegurarte siempre de que tu código se ajusta a tus requisitos. Una de estas herramientas, que te ayuda a hacer esto es pytest
y es probablemente la herramienta de pruebas más popular en el universo Python.
Se trata de assert
Supongamos que ha escrito una función que valida una dirección de correo electrónico. Tenga en cuenta que aquí lo mantenemos simple y no utilizamos Expresiones Regulares o pruebas de DNS para validar las direcciones de correo electrónico. En su lugar, sólo nos aseguramos de que haya exactamente un signo @
en la cadena a comprobar y sólo caracteres latinos, números y caracteres .
, -
y _
.
|
|
Ahora, tenemos algunas afirmaciones para nuestro código. Por ejemplo, afirmamos que estas direcciones de correo electrónico son válidas:
Por otro lado, esperaríamos que nuestra función devolviera False para direcciones de correo electrónico como:
not [email protected]
- contains spacejohn.doe
missing @ signjohn,[email protected]
contains comma
Podemos comprobar que nuestra función se comporta efectivamente como esperamos:
|
|
Estos ejemplos de direcciones de correo electrónico que se nos ocurren se denominan casos de prueba. Para cada caso de prueba, esperamos un resultado determinado. Una herramienta como pytest
puede ayudar a automatizar las pruebas de estas afirmaciones. Escribir estas aserciones puede ayudarte a
- documentar cómo se va a utilizar tu código
- asegurarse de que los futuros cambios no rompen otras partes de su software
- pensar en posibles casos límite de tus funcionalidades
Para ello, basta con crear un nuevo archivo para todas nuestras pruebas y poner unas cuantas funciones en él.
|
|
Ejecución de pruebas
Ejemplo fácil
Así, tenemos dos archivos en el directorio de nuestro proyecto: validator.py
y test_validator.py
.
Ahora podemos ejecutar simplemente pytest
desde la línea de comandos. Su salida debería ser algo así:
|
|
Aquí pytest
nos informa de que ha encontrado tres funciones de prueba dentro de test_validator.py
y que todas ellas han sido superadas (como indican los tres puntos ...
).
El indicador 100%
nos da una buena sensación ya que estamos seguros de que nuestro validador funciona como se espera. Sin embargo, como se indica en la introducción, la función del validador está lejos de ser perfecta. Y también lo son nuestros casos de prueba. Incluso sin las pruebas de DNS, marcaríamos como válida una dirección de correo electrónico como [email protected]
, mientras que una dirección como [email protected]
se marcaría como inválida.
Añadamos ahora estos casos de prueba a nuestro test_validator.py
.
|
|
Si ejecutamos de nuevo pytest
, vemos que las pruebas fallan:
|
|
Observe que tenemos dos FF
además de nuestros tres puntos ...
para indicar que dos funciones de prueba fallaron.
Además, obtenemos una nueva sección FAILURES
en nuestra salida que explica en detalle en qué punto falló nuestra prueba. Esto es bastante útil para la depuración.
Diseño de pruebas
Nuestro pequeño ejemplo de validador es un testimonio de la importancia de diseñar pruebas.
Primero escribimos nuestra función validadora y luego ideamos algunos casos de prueba para ella. Pronto nos dimos cuenta de que estos casos de prueba no son en absoluto completos. Por el contrario, nos faltaban algunos aspectos esenciales de la validación de una dirección de correo electrónico.
Puede que hayas oído hablar del Desarrollo Dirigido por Pruebas (TDD), que aboga por todo lo contrario: Conseguir que los requisitos sean correctos escribiendo primero los casos de prueba y no empezar a implementar una función antes de sentir que se han cubierto todos los casos de prueba. Esta forma de pensar siempre ha sido una buena idea, pero ha cobrado aún más importancia con el tiempo, ya que los proyectos de software han aumentado su complejidad.
Pronto escribiré otra entrada en el blog sobre TDD para tratarla en profundidad.
Configuración
Normalmente, la configuración de un proyecto es mucho más complicada que un simple archivo con una función de validación en él.
Es posible que tengas una estructura de paquetes de Python para tu proyecto, o que tu código dependa de dependencias externas como una base de datos.
Fixtures
Es posible que hayas utilizado el término fixture en diferentes contextos. Por ejemplo, para el Django web framework, los fixtures se refieren a una colección de datos iniciales que se cargan en la base de datos. Sin embargo, en el contexto de pytest
, los fixtures sólo se refieren a las funciones ejecutadas por pytest
antes y/o después de las funciones de prueba reales.
Montaje y desmontaje
Podemos crear estas funciones utilizando el decorador pytest.fixture()
. Por ahora lo hacemos dentro del archivo test_validator.py
.
|
|
Ten en cuenta que la configuración de la base de datos y su desmontaje se realizan en el mismo fixture. La palabra clave yield
indica la parte donde pytest
ejecuta las pruebas reales.
Para que el fixture sea realmente utilizado por una de tus pruebas, simplemente añade el nombre del fixture como argumento, así (todavía en test_validator.py
):
|
|
Obtención de datos de Fixtures
En lugar de utilizar yield
, una función de fijación también puede devolver valores arbitrarios:
|
|
De nuevo, solicitar ese fixture desde una función de prueba se hace proporcionando el nombre del fixture como parámetro:
|
|
Archivos de configuración
pytest
puede leer su configuración específica del proyecto desde uno de estos archivos:
pytest.ini
tox.ini
setup.cfg
El archivo a utilizar depende de las otras herramientas que pueda utilizar en su proyecto. Si has empaquetado tu proyecto, deberías usar el archivo setup.cfg
. Si usas tox para probar tu código en diferentes entornos, puedes poner la configuración de pytest
en el archivo tox.ini
. El archivo pytest.ini
se puede utilizar si no quieres utilizar ninguna herramienta adicional, sino pytest
.
El archivo de configuración es prácticamente el mismo para cada uno de estos tres tipos de archivos:
Using pytest.ini and tox.ini
|
|
Si está utilizando el archivo setup.cfg, la única diferencia es que debe anteponer a la sección [pytest]
el prefijo tool:
de esta manera:
|
|
conftest.py
Cada carpeta que contiene archivos de prueba puede contener un archivo conftest.py
que es leído por pytest
. Este es un buen lugar para colocar sus accesorios personalizados ya que estos podrían ser compartidos entre diferentes archivos de prueba.
Los archivos conftest.py
pueden alterar el comportamiento de pytest
en función del proyecto.
Aparte de los fixtures compartidos, puedes colocar hooks externos y plugins o modificadores para el PATH
utilizado por pytest
para descubrir pruebas y código de implementación.
CLI / PDB
Durante el desarrollo, principalmente cuando escribes tus pruebas antes de la implementación, pytest
puede ser una herramienta beneficiosa para la depuración.
Vamos a echar un vistazo a las opciones de línea de comandos más útiles.
Ejecutar sólo una prueba
Si quiere ejecutar sólo una prueba en particular, puede hacer referencia a esa prueba mediante el archivo test_
en el que se encuentra y el nombre de la función:
|
|
Sólo para recoger
A veces sólo se quiere tener una lista de la colección de pruebas en lugar de ejecutar todas las funciones de prueba.
|
|
Salir al primer error
Puede forzar a pytest
a dejar de ejecutar más pruebas después de una fallida:
|
|
Ejecutar sólo la última prueba fallida
Si quiere ejecutar sólo las pruebas que fallaron la última vez, puede hacerlo utilizando la bandera --lf
:
|
|
Ejecute todas las pruebas, pero ejecute primero las últimas que hayan fallado
|
|
Mostrar los valores de las variables locales en la salida
Si configuramos una función de prueba más compleja con algunas variables locales, podemos ordenar a pytest
que muestre estas variables locales con la bandera -l
.
Reescribamos nuestra función de prueba así:
|
|
Entonces,
|
|
nos dará este resultado:
|
|
Uso de pytest con un depurador
Hay un depurador de línea de comandos, llamado pdb
, que está integrado en Python. Puedes usar pytest
para depurar el código de tu función de prueba.
Si inicias pytest
con --pdb
, se iniciará una sesión de depuración pdb
justo después de que se produzca una excepción en tu prueba. La mayoría de las veces esto no es particularmente útil, ya que podrías querer inspeccionar cada línea de código antes de la excepción lanzada.
Otra opción es la bandera --trace
para pytest
que establecerá un punto de interrupción en la primera línea de cada función de prueba. Esto puede ser un poco incómodo si tienes muchas pruebas. Así que, para propósitos de depuración, una buena combinación es --lf --trace
que iniciaría una sesión de depuración con pdb
al principio de la última prueba que falló:
|
|
CI / CD
En los proyectos de software modernos, el software se desarrolla de acuerdo con los principios del Desarrollo Dirigido por Pruebas y se entrega a través de una tubería de Integración Continua / Despliegue Continuo que incluye pruebas automatizadas.
Una configuración típica es que los commits a la rama main/master
son rechazados a menos que todas las funciones de prueba pasen.
Si quieres saber más sobre el uso de pytest
en un entorno CI/CD, permanece atento porque estoy planeando un nuevo artículo sobre ese tema.
Documentación
La documentación oficial de pytest
está aquí: https://docs.pytest.org
Una nota rápida:
Este es un reenvío del artículo original de Bas Steins hecho con su permiso. Visita su sitio para ver más artículos y/o síguelo en Twitter: @bascodes