Conceptos básicos de Pytest

Completado

Comencemos a realizar pruebas con Pytest. Como hemos mencionado en la unidad anterior, Pytest es muy configurable y puede controlar conjuntos de pruebas complejos, pero no requiere mucho para empezar a escribir pruebas. De hecho, cuanto más sencillo sea escribir pruebas en un marco, mejor.

Al final de esta sección, tendrá todo lo necesario para empezar a escribir las primeras pruebas y ejecutarlas con Pytest.

Convenciones

Antes de profundizar en la escritura de pruebas, debemos cubrir algunas de las convenciones de prueba en las que se basa Pytest.

No hay muchas reglas difíciles sobre archivos de prueba, directorios de prueba o diseños de pruebas generales en Python. Al conocer estas reglas, puede aprovechar la detección y ejecución automáticas de pruebas sin necesidad de ninguna configuración adicional.

Archivos de prueba y directorio de pruebas

El directorio principal de las pruebas es el directorio tests. Puede colocar este directorio en el nivel raíz del proyecto, pero tampoco es inusual verlo junto con los módulos de código.

Nota

En este módulo, usaremos de forma predeterminada pruebas en la raíz de un proyecto.

Veamos cómo se ve la raíz de un pequeño proyecto de Python denominado jformat:

.
├── README.md
├── jformat
│   ├── __init__.py
│   └── main.py
├── setup.py
└── tests
    └── test_main.py

El directorio tests se encuentra en la raíz del proyecto con un único archivo de prueba. En este caso, el archivo de prueba se denomina test_main.py. En este ejemplo se muestran dos convenciones críticas:

  • Use un directorio tests para colocar archivos de prueba y directorios de pruebas anidados.
  • Prefijo de archivos de prueba con test. El prefijo indica que el archivo contiene código de prueba.

Precaución

Evite usar test (forma singular) como nombre de directorio. El nombre test es un módulo de Python, por lo que la creación de un directorio con el mismo nombre lo invalidaría. Use siempre el plural tests en su lugar.

Funciones de prueba

Un argumento seguro para usar Pytest es que permite escribir funciones de prueba. De forma similar a los archivos de prueba, las funciones de prueba deben tener el prefijo test_. El prefijo test_ garantiza que Pytest recopile la prueba y la ejecute.

Este es el aspecto de una función de prueba sencilla:

def test_main():
    assert "a string value" == "a string value"

Nota

Si está familiarizado con unittest, puede ser sorprendente ver el uso de assert en la función de prueba. Trataremos en profundidad las aserciones sin formato más adelante, pero con Pytest obtendrá informes de error enriquecidos con aserciones sin formato.

Clases de prueba y métodos de prueba

De forma similar a las convenciones para archivos y funciones, las clases de prueba y los métodos de prueba usan las siguientes convenciones:

  • Las clases de prueba tienen el prefijo Test
  • Los métodos de prueba tienen el prefijo test_

Una diferencia principal con la biblioteca unittest de Python es que no es necesaria la herencia.

En el siguiente ejemplo se usan estos prefijos y otras convenciones de nomenclatura de Python para clases y métodos. Muestra una clase de prueba pequeña que comprueba los nombres de usuario en una aplicación.

class TestUser:

    def test_username(self):
        assert default() == "default username"

Ejecutar pruebas

Pytest es a la vez un marco de pruebas y un ejecutor de pruebas. El ejecutor de pruebas es un ejecutable en la línea de comandos que (en un nivel alto) puede hacer lo siguiente:

  • Realice la recopilación de pruebas buscando todos los archivos, las clases y las funciones de prueba para una ejecución de prueba.
  • Inicie una ejecución de pruebas mediante la ejecución de todas las pruebas.
  • Realizar un seguimiento de los errores y pruebas en proceso.
  • Proporcione informes completos al final de una serie de pruebas.

Nota

Dado que Pytest es una biblioteca externa, debe instalarse para poder usarla.

Dados estos contenidos en un archivo test_main.py, podemos ver cómo se comporta Pytest mediante la ejecución de las pruebas:

# contents of test_main.py file

def test_main():
    assert True

En la línea de comandos, en la misma ruta de acceso donde está el archivo test_main.py, podemos ejecutar el ejecutable pytest:

 $ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item

test_main.py .                                                       [100%]

============================ 1 passed in 0.00s =============================

En segundo plano, Pytest recopila la prueba de ejemplo en el archivo de prueba sin necesidad de configuración.

Instrucción de aserción eficaz

Hasta ahora, todos los ejemplos de prueba usan la llamada sin formato assert. Normalmente, en Python, la assert instrucción no se usa para las pruebas, ya que carece de informes adecuados cuando se produce un error en la aserción. Sin embargo, Pytest no tiene esta limitación. En segundo plano, Pytest permite que la instrucción realice comparaciones enriquecidas sin forzar al usuario a escribir más código ni configurar nada.

Mediante la instrucción assert sin formato, puede usar los operadores de Python; por ejemplo, >, <, !=, >= o <=. Todos los operadores de Python son válidos. Esta funcionalidad puede ser la característica más crucial de Pytest: no es necesario aprender una nueva sintaxis para escribir aserciones.

Veamos cómo se traduce al tratar con comparaciones comunes con objetos de Python. En este caso vamos a pasar por el informe de errores al comparar cadenas largas:

================================= FAILURES =================================
____________________________ test_long_strings _____________________________

    def test_long_strings():
        left = "this is a very long strings to be compared with another long string"
        right = "This is a very long string to be compared with another long string"
>       assert left == right
E       AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E         - This is a very long string to be compared with another long string
E         ? ^
E         + this is a very long strings to be compared with another long string
E         ? ^                         +

test_main.py:4: AssertionError

Pytest muestra contexto útil en torno al error: un uso incorrecto de mayúsculas al principio de la cadena y un carácter adicional en una palabra. Pero más allá de las cadenas, Pytest puede ayudar con otros objetos y estructuras de datos. Por ejemplo, este es el comportamiento con las listas:

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E         At index 1 diff: 'wheat' != 'coffee'
E         Full diff:
E         - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E         ?                     ---------
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?           +++++++++

test_main.py:9: AssertionError

Este informe identifica que el índice 1 (segundo elemento de la lista) es diferente. No solo identifica el número de índice, sino que también proporciona una representación del error. Aparte de las comparaciones de elementos, también puede notificar si faltan elementos y proporcionar información que puede indicar exactamente qué elemento podría ser. En el siguiente caso, es "milk":

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E         At index 2 diff: 'coffee' != 'salt'
E         Left contains one more item: 'milk'
E         Full diff:
E         - ['sugar', 'wheat', 'salt', 'water', 'milk']
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?                    ++++++++++

test_main.py:9: AssertionError

Por último, veamos cómo se comporta con los diccionarios. Comparar dos diccionarios grandes puede ser abrumador si hay errores, pero Pytest realiza un trabajo sobresaliente al proporcionar contexto e identificar el error:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         {'number': 39} != {'number': 38}
E         Full diff:
E           {
E            'county': 'Frett',...
E
E         ...Full output truncated (12 lines hidden), use '-vv' to show

En esta prueba, hay dos errores en el diccionario. Uno es que el valor "street" es diferente y el otro es que "number" no coincide.

Pytest detecta con precisión estas diferencias (aunque se trata de un error en una sola prueba). Dado que los diccionarios contienen muchos elementos, Pytest omite las partes idénticas y solo muestra el contenido pertinente. Veamos lo que sucede si hacemos uso de la marca -vv sugerida para aumentar el nivel de detalle en el resultado:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E         Common items:
E         {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E         Differing items:
E         {'number': 39} != {'number': 38}
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         Full diff:
E           {
E            'county': 'Frett',
E         -  'number': 38,
E         ?             ^
E         +  'number': 39,
E         ?             ^
E            'state': 'Nevada',
E         -  'street': 'Ferry Lane',
E         ?                    - ^
E         +  'street': 'Ferry Ln.',
E         ?                     ^
E            'zipcode': 30877,
E           }

Al ejecutar pytest -vv, el informe aumenta la cantidad de detalles y proporciona una comparación pormenorizada. No solo este informe detecta y muestra el error, sino que le permite realizar rápidamente cambios para corregir el problema.