Esta guía trata sobre el framework de pruebas más popular entre los desarrolladores Python.
Escribir código funcional es solo la mitad del trabajo: debes asegurarte de que produzca resultados correctos.
Puedes probar tu programa manualmente: ejecutarlo una y otra vez en diferentes condiciones y verificar si todo funciona correctamente. Pero, por supuesto, es mejor automatizar el proceso y escribir código que verifique otro código. Para simplificar esta tarea, se creó Pytest.
¿Qué es Pytest?
Pytest es un framework para probar código en Python. Fue desarrollado en 2004, pero aún se actualiza regularmente y permite no solo escribir pruebas, sino también crear un entorno para ellas, así como configurar los parámetros de ejecución.
Según una investigación de JetBrains, Pytest lo usa uno de cada dos programadores Python.
Ventajas y Desventajas de Pytest
El éxito de Pytest en comparación con sus competidores (como Unittest) se explica fácilmente por sus ventajas:
- Código conciso: El sintaxis de Pytest no tiene estructuras engorrosas como en Unittest. Una prueba simple puede consistir en solo dos líneas.
- Informes de errores detallados: Si una prueba no funciona correctamente, Pytest te explicará qué sucede.
- Operador
assertuniversal: No necesitas recordar sus diferentes tipos, como en Unittest. - Fixtures: Permiten crear contexto para un grupo de pruebas.
- Etiquetas: Puedes configurar el comportamiento de las pruebas: establecer condiciones de ejecución, pasar diferentes datos de entrada a la misma prueba, etc.
- Puede ejecutar pruebas de otros frameworks: Pytest es compatible con Unittest, Doctest y Nose.
- Muchos plugins: Si te falta alguna función “fuera de la caja”, hay más de mil plugins escritos para Pytest.
Sin embargo, el framework también tiene desventajas:
- Implícito y mágico: La otra cara de la simplicidad y la concisión es que muchos procesos ocurren “detrás del capó”. Para comprenderlos en detalle, tendrás que estudiar la documentación.
- No está incluido en la biblioteca estándar: Pytest debe instalarse por separado. Si tienes una versión antigua (inferior a 3.7) de Python, deberás conectar la versión correspondiente del framework. Puedes encontrar la lista aquí.
- Otros frameworks no son compatibles con Pytest: Una consecuencia inevitable de su estatus de líder: Pytest puede ejecutar pruebas de otros frameworks, pero ninguno de los otros frameworks puede ejecutar pruebas de Pytest.
Cómo instalar Pytest
Pytest se incluye en la mayoría de los paquetes de Python. Su última versión está disponible para Python 3.7+ y PyPy 3. Para instalarlo en tu entorno virtual, usa el comando:
pip install -U pytestOtra forma es usar el gestor de paquetes de tu IDE. Busca el módulo llamado pytest y cárgalo.
Cómo escribir pruebas
Primero, necesitas el código que vas a probar. Crea un archivo main.py y la función sum2. Recibirá dos argumentos y devolverá su suma:
def sum2(x, y):
return x + yAhora, verifica si funciona correctamente. Para ello, crea un archivo tests.py, importa sum2 y escribe test_sum2:
from main import sum2
def test_sum2():
assert sum2(15, 8) == 23Para ejecutar las pruebas, introduce el comando pytest en la consola. Una alternativa es usar la interfaz de tu IDE. Por ejemplo, PyCharm permite ejecutar el archivo completo o una función de prueba individualmente.
Obtendrás este resultado:
tests.py::test_sum2 PASSED [100%]Ahora cambia tu prueba: espera obtener 0 en lugar de 23:
def test_sum2():
assert sum2(15, 8) == 0Recibirás un mensaje indicando que la prueba no se ha superado:
FAILED [100%]
tests.py:3 (test_sum2)
23 != 0
Expected :0
Actual :23
<Click to see difference>
def test_sum2():
> assert sum2(15, 8) == 0
E assert 23 == 0
E + where 23 = sum2(15, 8)
tests.py:5: AssertionErrorRestricciones de Nomenclatura
Para que Pytest reconozca las funciones como pruebas, los archivos y las propias pruebas deben nombrarse de una manera específica:
- El nombre del archivo debe comenzar con
testo terminar contest.py. - El nombre de la función debe estar en minúsculas y comenzar con
test_.
Cómo funciona assert
A la palabra clave assert se le puede pasar cualquier condición. Si es verdadera (resultado True), la prueba se aprueba; si es falsa (resultado False), no se aprueba.
De esta manera, puedes escribir pruebas mínimas:
def test_true():
assert True
def test_false():
assert FalseResultado:
tests.py::test_true PASSED [50%]
tests.py::test_false FAILED [100%]
tests.py:9 (test_false)
def test_false():
> assert False
E assert False
tests.py:11: AssertionErrorPuedes agregar un mensaje de depuración después de la condición con una coma. Pytest lo mostrará si la prueba falla:
def test_message():
assert False, 'La prueba siempre falla'Resultado:
tests.py::test_message FAILED [100%]
tests.py:0 (test_message)
def test_message():
> assert False, 'La prueba siempre falla'
E AssertionError: La prueba siempre falla
E assert False
tests.py:2: AssertionErrorSi una prueba no tiene assert, se considera aprobada:
def test_pass():
pass # instrucción de relleno, no hace nadaResultado:
tests.py::test_pass PASSED [100%]Nota: Una prueba puede tener varios operadores assert, pero no es recomendable. Es mejor seguir la regla “Una prueba – una entidad, una función – un assert“.
Ejecución de pruebas
El comando de terminal pytest ejecuta todas las pruebas del directorio actual. Para controlar las condiciones de ejecución, especifica después de él la ruta al archivo o incluso a una función específica.
- Comando para ejecutar el archivo
tests.py:pytest tests.py. - Comando para ejecutar solo la función
test_sum2:pytest tests.py::test_sum2.
Para una ejecución más flexible, puedes agregar banderas. Su lista se encuentra en la documentación de Pytest.
Además de los comandos de terminal, puedes usar la interfaz gráfica de tu IDE. Las pruebas descritas en este artículo se ejecutan a través de las herramientas de PyCharm.
Fixtures en Pytest
Los fixtures son funciones que crean un entorno alrededor de las pruebas. Son útiles cuando necesitas pasar los mismos datos de entrada a varias pruebas.
Supongamos que tienes varias funciones en main.py:
# Agrega 2 a cada elemento de la colección
def plus2(nums):
result = []
for num in nums:
result.append(num + 2)
return result
# Multiplica por 2 cada elemento de la colección
def multiply2(nums):
result = []
for num in nums:
result.append(num * 2)
return result
# Eleva al cuadrado cada elemento de la colección
def exponent2(nums):
result = []
for num in nums:
result.append(num ** 2)
return resultEscribe una prueba para cada una de ellas en el archivo tests.py. Como matriz de prueba, usa una lista de números primos del 1 al 50. Créala usando un bucle for-else:
from main import *
def test_plus2():
prime_nums = []
for num in range(1, 50):
for div in range(2, num):
if num % div == 0:
break
else:
prime_nums.append(num)
assert plus2(prime_nums) == [3, 4, 5, 7, 9, 13, 15, 19, 21, 25, 31, 33, 39, 43, 45, 49]
def test_multiply2():
prime_nums = []
for num in range(1, 50):
for div in range(2, num):
if num % div == 0:
break
else:
prime_nums.append(num)
assert multiply2(prime_nums) == [2, 4, 6, 10, 14, 22, 26, 34, 38, 46, 58, 62, 74, 82, 86, 94]
def test_exponent2():
prime_nums = []
for num in range(1, 50):
for div in range(2, num):
if num % div == 0:
break
else:
prime_nums.append(num)
assert exponent2(prime_nums) == [1, 4, 9, 25, 49, 121, 169, 289, 361, 529, 841, 961, 1369, 1681, 1849, 2209]Por ahora, todas las funciones de prueba usan la misma estructura engorrosa que crea una lista de números primos. Sácala a una fixture separada. Para ello, importa explícitamente el módulo pytest:
import pytestPara declarar una función como fixture, usa el decorador @pytest.fixture() antes de ella:
@pytest.fixture()
def get_prime_nums():
prime_nums = []
for num in range(1, 50):
for div in range(2, num):
if num % div == 0:
break
else:
prime_nums.append(num)
return prime_numsAhora, pasa esta fixture a todas las pruebas donde sea necesaria. Al acceder a la fixture, no necesitas usar paréntesis: como si fuera una variable, no una función.
Las pruebas resultantes son:
def test_plus2(get_prime_nums):
prime_nums = get_prime_nums
assert plus2(prime_nums) == [3, 4, 5, 7, 9, 13, 15, 19, 21, 25, 31, 33, 39, 43, 45, 49]
def test_multiply2(get_prime_nums):
prime_nums = get_prime_nums
assert multiply2(prime_nums) == [2, 4, 6, 10, 14, 22, 26, 34, 38, 46, 58, 62, 74, 82, 86, 94]
def test_exponent2(get_prime_nums):
prime_nums = get_prime_nums
assert exponent2(prime_nums) == [1, 4, 9, 25, 49, 121, 169, 289, 361, 529, 841, 961, 1369, 1681, 1849, 2209]Finalizador
Si deseas que se ejecute otro script después de ejecutar una prueba, también puedes hacerlo a través de fixtures. Para ello, usa yield en lugar de return. El código escrito después de yield se ejecutará al finalizar la prueba.
Cambia la fixture get_prime_nums() en nuestro ejemplo con números primos y agrega un finalizador:
@pytest.fixture()
def get_prime_nums():
print('\nTrabajo de la fixture')
prime_nums = []
for num in range(1, 50):
for div in range(2, num):
if num % div == 0:
break
else:
prime_nums.append(num)
yield prime_nums
print('\nTrabajo del finalizador')Al ejecutar las pruebas, obtienes este resultado:
tests.py::test_plus2
Trabajo de la fixture
PASSED [ 33%]
Trabajo del finalizador
tests.py::test_multiply2
Trabajo de la fixture
PASSED [ 66%]
Trabajo del finalizador
tests.py::test_exponent2
Trabajo de la fixture
PASSED [100%]
Trabajo del finalizadorSi una prueba no se ha superado (es decir, assert recibió False), el código del finalizador se ejecuta de todos modos.
Alcance de las Fixtures
El alcance de las fixtures se puede configurar. Por defecto, es la función. Esto significa que cuando una función de prueba termina su trabajo, la fixture se finaliza y destruye. En la siguiente llamada, la fixture se crea de nuevo. Esto se ve claramente en el ejemplo anterior con el finalizador.
El alcance de la fixture se especifica en su decorador con el argumento scope='alcance'. Hay cinco niveles:
'function'– para la función;'class'– para la clase;'module'– para el módulo (es decir, archivo .py);'package'– para el paquete;'session'– para toda la sesión de prueba.
Cambia el alcance de la fixture get_prime_nums a module.
@pytest.fixture(scope='module')
def get_prime_nums():
print('\nTrabajo de la fixture')
prime_nums = []
for num in range(1, 50):
for div in range(2, num):
if num % div == 0:
break
else:
prime_nums.append(num)
yield prime_nums
print('\nTrabajo del finalizador')Veamos cómo cambia el trabajo de las pruebas:
tests.py::test_plus2
Trabajo de la fixture
PASSED [ 33%]
tests.py::test_multiply2 PASSED [ 66%]
tests.py::test_exponent2 PASSED [100%]
Trabajo del finalizadorA diferencia del ejemplo anterior, la fixture se llama solo una vez, en la primera función que la usa. Luego, el resultado del trabajo se almacena en caché. El finalizador también se activa solo una vez, cuando termina el archivo.
Jerarquías de Fixtures
A una prueba se le pueden pasar tantas fixtures como se desee, indicándolas con comas. También se pueden pasar a otras fixtures, también en cualquier cantidad.
Por ejemplo, la fixture get_prime_nums se puede dividir en varias (aunque en nuestro caso no tiene sentido práctico):
@pytest.fixture()
def get_min_num():
return 1
@pytest.fixture()
def get_max_num():
return 50
@pytest.fixture()
def get_prime_nums(get_min_num, get_max_num):
prime_nums = []
for num in range(get_min_num, get_max_num):
for div in range(2, num):
if num % div == 0:
break
else:
prime_nums.append(num)
return prime_numsSiempre indicas explícitamente qué fixtures usas en una función u otra fixture. Esto permite rastrear cómodamente las dependencias de datos y controlarlas.
Autouso de Fixtures
A veces puede ser útil que una fixture se ejecute siempre, incluso si la función no la llama. Por ejemplo, cuando necesitas iniciar sesión en un sistema antes de ejecutar las pruebas.
En estos casos, puedes especificar el parámetro autouse en el decorador: @pytest.fixture(autouse=True).
Ten cuidado al usar autouse. Estas fixtures pueden crear dependencias implícitas y cambiar los datos de una manera impredecible. Especialmente si hay muchas y están en una jerarquía compleja.
Etiquetas de Pruebas
Pytest permite configurar la ejecución de pruebas aplicándoles etiquetas. Se pueden usar no solo con funciones de prueba, sino también con clases completas. Para agregar una etiqueta, escribe un decorador: @pytest.mark.*nombre de la etiqueta*.
En Pytest, puedes hacer que se ejecuten solo las pruebas etiquetadas. Para ello, usa el comando de terminal con el argumento -m: pytest -m *nombre de la etiqueta*. Puedes hacer lo contrario: ejecutar todas las pruebas, excepto las etiquetadas. En este caso, el comando tiene este aspecto: pytest -m 'not nombre de la etiqueta'.
Una prueba o clase puede tener tantas etiquetas como sea necesario. Puedes ver su lista con el comando pytest --markers y en la documentación. Hablaremos de las principales.
Omitir una Prueba
Para omitir una prueba, coloca la etiqueta skip. Como argumento, puedes pasar un parámetro opcional reason='razón de la omisión'. Por ejemplo:
@pytest.mark.skip(reason='Omisión de prueba')
def test_skipped():
passResultado:
SKIPPED (Omisión de prueba) [100%]
Skipped: Omisión de pruebaOmitir una Prueba bajo Condición
La etiqueta skipif recibe dos argumentos. El primero es una condición. Si se cumple (resultado True), la prueba se omite; si no (resultado False), la prueba se ejecuta normalmente. En el segundo argumento, al igual que con skip, puedes pasar una cadena con la razón de la omisión:
x = 1
@pytest.mark.skipif(x > 0, reason='Omisión de prueba')
def test_skipped_if():
passEl resultado es el mismo que en el caso anterior.
Fallo Esperado de una Prueba
Una prueba con la etiqueta xfail puede dar dos resultados. Si la prueba se aprueba, Pytest la marcará como XPASS; si falla como se espera, como XFAIL. Ninguna de las opciones provocará un fallo en el conjunto general de pruebas:
@pytest.mark.xfail(reason='Fallo intencionado')
def test_xfailed():
assert FalseResultado:
XFAIL (Fallo intencionado) [100%]
@pytest.mark.xfail(reason='Fallo intencionado')
def test_xfailed():
> assert False
E assert False
tests.py:49: AssertionErrorxfail tiene varios argumentos. Al igual que en skipif, puedes especificar una condición (y esperar un fallo solo con ella) y pasar el parámetro reason. Además, xfail permite:
- Agregar una excepción en
raises=*nombre de la excepción*; - No ejecutar la prueba en
run=False(entonces se contará automáticamente comoXFAIL); - Hacer que el fallo de la prueba provoque el fallo de todo el conjunto de pruebas en
strict=True.
Más información sobre las capacidades de xfail en la documentación.
Parametrización
La etiqueta parametrize permite llamar a la misma prueba con diferentes datos de entrada. Esto es útil cuando quieres comprobar varios casos.
Por ejemplo, tienes una función que escribe si un número es positivo o negativo:
def positive_or_negative(x):
if x > 0:
return 'positive'
elif x < 0:
return 'negative'
else:
return 'zero'Primero, verifica si procesa correctamente los números positivos: enteros, decimales y muy pequeños. Sin parametrización, tendrías que escribir tres pruebas del mismo tipo:
from main import positive_or_negative
def test_positive_or_negative_if_positive_int():
assert positive_or_negative(165) == 'positive'
def test_positive_or_negative_if_positive_float():
assert positive_or_negative(1.2) == 'positive'
def test_positive_or_negative_if_positive_small():
assert positive_or_negative(0.0000001) == 'positive'Resultado:
tests.py::test_positive_or_negative_if_positive_int PASSED [ 33%]
tests.py::test_positive_or_negative_if_positive_float PASSED [ 66%]
tests.py::test_positive_or_negative_if_positive_small PASSED [100%]Luego, tendrías que escribir tres pruebas similares para números negativos y otra para cero. En total, siete pruebas para una función pequeña. No es eficiente. Aquí es donde ayuda la parametrización.
La etiqueta parametrize recibe dos argumentos: el nombre de la variable y la lista de sus valores. El nombre de la variable se pasa a la prueba (de la misma manera que la fixture). Queda así:
import pytest
from main import positive_or_negative
@pytest.mark.parametrize('x', [165, 1.2, 0.0000001])
def test_positive_or_negative_if_positive(x):
assert positive_or_negative(x) == 'positive'Resultado:
tests.py::test_positive_or_negative_if_positive[165]
tests.py::test_positive_or_negative_if_positive[1.2]
tests.py::test_positive_or_negative_if_positive[1e-07]
PASSED [33%]
PASSED [66%]
PASSED [100%]Puedes pasar varias variables en una línea con comas. Entonces, cada elemento de la lista se define con una tupla, donde enumeramos los valores de estas variables secuencialmente:
import pytest
from main import positive_or_negative
@pytest.mark.parametrize('x, expected_result',
[(165, 'positive'),
(1.2, 'positive'),
(0.0000001, 'positive'),
(-165, 'negative'),
(-1.2, 'negative'),
(-0.0000001, 'negative'),
(0, 'zero')])
def test_positive_or_negative(x, expected_result):
assert positive_or_negative(x) == expected_resultResultado:
tests.py::test_positive_or_negative[165-positive]
tests.py::test_positive_or_negative[1.2-positive]
tests.py::test_positive_or_negative[1e-07-positive]
tests.py::test_positive_or_negative[-165-negative]
tests.py::test_positive_or_negative[-1.2-negative]
tests.py::test_positive_or_negative[-1e-07-negative]
tests.py::test_positive_or_negative[0-zero]
PASSED [14%]
PASSED [28%]
PASSED [42%]
PASSED [57%]
PASSED [71%]
PASSED [85%]
PASSED [100%]Si se pasan varias etiquetas parametrize a una prueba, ejecutará todas sus posibles combinaciones. Ejemplo para mayor claridad:
import pytest
@pytest.mark.parametrize('x', [1, 0, -1])
@pytest.mark.parametrize('y', [1, 0, -1])
def test_coordinate_zone(x, y):
print(x, y)Resultado:
tests.py::test_coordinate_zone[1-1]
tests.py::test_coordinate_zone[1-0]
tests.py::test_coordinate_zone[1--1]
tests.py::test_coordinate_zone[0-1]
tests.py::test_coordinate_zone[0-0]
tests.py::test_coordinate_zone[0--1]
tests.py::test_coordinate_zone[-1-1]
tests.py::test_coordinate_zone[-1-0]
tests.py::test_coordinate_zone[-1--1]
PASSED [11%]1 1
PASSED [22%]0 1
PASSED [33%]-1 1
PASSED [44%]1 0
PASSED [55%]0 0
PASSED [66%]-1 0
PASSED [77%]1 -1
PASSED [88%]0 -1
PASSED [100%]-1 -1Etiquetas Personalizadas
Además de las etiquetas integradas, puedes usar tus propias etiquetas. Esto es útil cuando necesitas dividir todas las pruebas en varios grupos y ejecutarlas por separado.
Para crear tu propia etiqueta, simplemente usa su nombre:
import pytest
@pytest.mark.my_mark
def test_1():
pass
def test_2():
pass
@pytest.mark.my_mark
def test_3():
pass
@pytest.mark.my_mark
def test_4():
pass
def test_5():
passUsa el comando pytest --no-summary -m my_mark tests.py para ejecutar solo las pruebas con la etiqueta my_mark. Obtendrás este resultado:
collected 5 items / 2 deselected / 3 selected
tests.py .. [100%]
===== 3 passed, 2 deselected, 3 warnings in 0.02s =====Como puedes ver, de cinco archivos, solo se ejecutaron tres, aquellos con la etiqueta necesaria.
También obtuvimos tres advertencias. Pytest nos llama la atención sobre el hecho de que la etiqueta my_mark no está registrada en el archivo pytest.ini. Pregunta: ¿Estás seguro de que usaste tu propia etiqueta y no te equivocaste al escribir una integrada?
Puedes leer cómo registrar tu propia etiqueta en la documentación.
Resumen
Pytest es el framework de prueba más popular entre los programadores Python. Permite escribir menos código repetitivo que Unittest y puede funcionar sin clases de prueba. Estas son sus herramientas básicas:
- La palabra clave
assertes responsable del resultado de la prueba. Si la condición especificada después de ella es verdadera, la prueba se aprueba; si es falsa, falla. - Las fixtures son funciones adicionales en las que puedes configurar el entorno de las pruebas. Pueden usar otras fixtures, creando jerarquías completas.
- Las etiquetas son decoradores que permiten modificar el comportamiento de las pruebas: omitirlas, esperar resultados específicos, pasar diferentes datos de entrada, etc. Puedes crear tus propias etiquetas personalizadas.