Cómo realizar pruebas unitarias en Flask

Introducción

La prueba es fundamental en el proceso de desarrollo de software, ya que garantiza que el código se comporta como se espera y está libre de defectos. En Python, pytest es un popular marco de pruebas que ofrece varias ventajas sobre el módulo estándar unit test, que es un marco de pruebas integrado de Python y forma parte de la biblioteca estándar. pytest incluye una sintaxis más simple, mejores salidas, fixtures potentes y un rico ecosistema de plugins. Este tutorial le guiará a través de la configuración de una aplicación de Flask, la integración de fixtures de pytest y la escritura de pruebas unitarias utilizando pytest.

Prerrequisitos

Antes de empezar, necesitará lo siguiente:

  • Un servidor que ejecute Ubuntu y un usuario no root con privilegios de sudo y una cortina activa. Para obtener instrucciones sobre cómo configurar esto, elija su distribución de la lista este listado y siga nuestra guía de configuración inicial del servidor. Asegúrese de trabajar con una versión compatible de Ubuntu.

  • Familiaridad con la línea de comandos de Linux. Puede visitar esta guía sobre el primer curso de la línea de comandos de Linux.

  • Un entendimiento básico de la programación en Python y del marco de pruebas pytest en Python. Puede referirse a nuestro tutorial sobre el Marco de Pruebas en Python pytest para obtener más información sobre pytest.

  • Python 3.7 o posterior instalado en su sistema Ubuntu. Para aprender cómo ejecutar un script de Python en Ubuntu, puede referirse a nuestro tutorial sobre Cómo ejecutar un script de Python en Ubuntu.

Por qué pytest es una Alternativa Mejor que unittest

pytest ofrece varias ventajas sobre el framework integrado unittest:

  • Pytest le permite escribir pruebas con menos código debecho, utilizando simples declaraciones assert en lugar de los métodos más verbosos requeridos por unittest.

  • Proporciona un resultado más detallado y legible, haciendo más fácil identificar dónde y por qué falló una prueba.

  • Las fijaciones de Pytest permiten configuraciones de prueba más flexibles y reutilizables que los métodos setUp y tearDown de unittest.

  • Facilita la ejecución de la misma función de prueba con múltiples conjuntos de entrada, lo cual no es tan directo en unittest.

  • Pytest posee una rica colección de plugins que extienden su funcionalidad, desde herramientas de cobertura de código hasta ejecución de pruebas en paralelo.

  • Automáticamente descubre archivos de prueba y funciones que cumplen con sus convenciones de nomenclatura, economizando tiempo y esfuerzo en la gestión de suites de pruebas.

Dada estas ventajas, pytest a menudo es la opción preferida para las pruebas de Python moderno. Vamos a configurar una aplicación de Flask y escribir pruebas unitarias usando pytest.

Paso 1 – Configuración del Entorno

Ubuntu 24.04 trae Python 3 por defecto. Abra el terminal y ejecute el siguiente comando para comprobar la instalación de Python 3:

root@ubuntu:~# python3 --version
Python 3.12.3

Si Python 3 ya está instalado en su equipo, el comando anterior devolverá la versión actual de la instalación de Python 3. En caso de que no esté instalado, puede ejecutar el siguiente comando para obtener la instalación de Python 3:

root@ubuntu:~# sudo apt install python3

A continuación, necesitará instalar el instalador de paquetes pip en su sistema:

root@ubuntu:~# sudo apt install python3-pip

Una vez que pip esté instalado, vamos a instalar Flask.

Paso 2 – Crear una Aplicación de Flask

Empecemos por crear una aplicación de Flask simple. Cree un nuevo directorio para su proyecto y navegue hasta él:

root@ubuntu:~# mkdir flask_testing_app
root@ubuntu:~# cd flask_testing_app

Ahora, vamos a crear y activar un entorno virtual para gestionar las dependencias:

root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# source venv/bin/activate

Instale Flask usando pip:

root@ubuntu:~# pip install Flask

Ahora, vamos a crear una aplicación de Flask simple. Cree un archivo nuevo llamado app.py y agregue el código siguiente:

app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello, Flask!")

@app.route('/about')
def about():
    return jsonify(message="This is the About page")

@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
    result = x * y
    return jsonify(result=result)

if __name__ == '__main__':
    app.run(debug=True)

Esta aplicación tiene tres rutas:

  • /: Devuelve un mensaje simple “Hello, Flask!”.
  • /about: Devuelve un mensaje simple “This is the About page”.
  • /multiply/<int:x>/<int:y>: Multiplica dos enteros y devuelve el resultado.

Para ejecutar la aplicación, ejecute el siguiente comando:

root@ubuntu:~# flask run
output
* Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Del resultado de arriba puede notar que el servidor se está ejecutando en http://127.0.0.1 y está escuchando en el puerto 5000. Abra otra consola de Ubuntu y ejecute los siguientes comandos curl uno por uno:

  • GET: curl http://127.0.0.1:5000/
  • GET: curl http://127.0.0.1:5000/about
  • GET: curl http://127.0.0.1:5000/multiply/10/20

Vamos a comprender qué hacen estas solicitudes GET:

  1. curl http://127.0.0.1:5000/:
    Esta envía una solicitud GET a la ruta raíz (‘/’) de nuestra aplicación Flask. El servidor responde con un objeto JSON que contiene el mensaje “Hello, Flask!”, demostrando la funcionalidad básica de nuestra ruta principal.

  2. curl http://127.0.0.1:5000/about:
    Esta envía una solicitud GET a la ruta /about. El servidor responde con un objeto JSON que contiene el mensaje “This is the About page”. Esto muestra que nuestra ruta está funcionando correctamente.

  3. curl http://127.0.0.1:5000/multiply/10/20:
    Esta envía una solicitud GET a la ruta /multiply con dos parámetros: 10 y 20. El servidor multiplica estos números y responde con un objeto JSON que contiene el resultado (200). Esto demuestra que nuestra ruta de multiplicación puede procesar correctamente los parámetros de URL y realizar cálculos.

Estas solicitudes GET nos permiten interactuar con las API de los puntos finales de nuestra aplicación Flask, obteniendo información o disparando acciones en el servidor sin modificar ningún dato. Son útiles para recuperar datos, probar la funcionalidad de los puntos finales, y verificar que nuestras rutas responden como se espera.

Veamos cada una de estas solicitudes GET en acción:

root@ubuntu:~# curl http://127.0.0.1:5000/
Output
{"message":"Hello, Flask!"}
root@ubuntu:~# curl http://127.0.0.1:5000/about
Output
{"message":"This is the About page"}
root@ubuntu:~# curl http://127.0.0.1:5000/multiply/10/20
Output
{"result":200}

Paso 3 – Instalando pytest y Escribiendo Tu Primera Prueba

Ahora que tienes una aplicación Flask básica, vamos a instalar pytest y escribir algunas pruebas unitarias.

Instala pytest mediante pip:

root@ubuntu:~# pip install pytest

Crea un directorio llamado tests para almacenar tus archivos de prueba:

root@ubuntu:~# mkdir tests

Ahora, vamos a crear un nuevo archivo llamado test_app.py y agregamos el siguiente código:

test_app.py
# Importar el módulo sys para modificar el entorno de tiempo de ejecución de Python
import sys
# Importar el módulo os para interactuar con el sistema operativo
import os

# Agregar la carpeta padre a sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Importar la instancia de Flask app desde el archivo principal de app
from app import app 
# Importar pytest para escribir y ejecutar pruebas
import pytest

@pytest.fixture
def client():
    """A test client for the app."""
    with app.test_client() as client:
        yield client

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Vamos a desglosar las funciones en este archivo de prueba:

  1. @pytest.fixture def client():
    Este es un proveedor de pruebas de pytest que crea un cliente de prueba para nuestra aplicación Flask. Utiliza el método app.test_client() para crear un cliente que puede enviar solicitudes a nuestra aplicación sin ejecutar el servidor real. La declaración yield permite que el cliente sea utilizado en pruebas y luego cerrado correctamente después de cada prueba.

  2. def test_home(client):
    Esta función prueba la ruta de inicio (/) de nuestra aplicación. Envía una solicitud GET a la ruta utilizando el cliente de prueba, luego afirma que el código de estado de la respuesta es 200 (OK) y que la respuesta JSON coincide con el mensaje esperado.

  3. def test_about(client):
    Parecido a test_home, esta función prueba la ruta de acerca de (/about). Comprueba un código de estado de 200 y verifica el contenido de la respuesta JSON.

  4. def test_multiply(client):
    Esta función prueba la ruta de multiplicar con un input válido (/multiply/3/4). Comprueba que el código de estado es 200 y que la respuesta JSON contiene el resultado correcto de la multiplicación.

  5. def test_multiply_invalid_input(client):
    Esta función prueba la ruta de multiplicación con un input no válido (multiply/three/four). Comprueba que el código de estado es 404 (No Encontrado), lo cual es el comportamiento esperado cuando la ruta no puede concordar las entradas de cadena con los parámetros enteros requeridos.

  6. def test_non_existent_route(client):
    Esta función prueba el comportamiento de la aplicación cuando se accede a una ruta que no existe. Envia una solicitud GET a /non-existent, que no está definida en nuestra aplicación Flask. La prueba afirma que el código de estado de la respuesta es 404 (No Encontrado), garantizando que nuestra aplicación maneja correctamente las solicitudes a rutas no definidas.

Estas pruebas cubren la funcionalidad básica de nuestra aplicación Flask, garantizando que cada ruta responda correctamente a entradas válidas y que la ruta de multiplicación maneja inputs no válidos de manera apropiada. Mediante el uso de pytest, podemos ejecutar fácilmente estas pruebas para verificar que nuestra aplicación funciona como se espera.

Paso 4 – Ejecución de las Pruebas

Para ejecutar las pruebas, ejecute el siguiente comando:

root@ubuntu:~# pytest

Por defecto, el proceso de descubrimiento de pytest buscará de forma recursiva las carpetas actuales y sus subcarpetas buscando archivos que empiezan con el nombre “test_” o que terminen con “_test”. Las pruebas ubicadas en estos archivos se ejecutarán entonces. Debería ver salida similar a:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app collected 5 items tests/test_app.py .... [100%] ======================================================= 5 passed in 0.19s ========================================================

Esto indica que todas las pruebas se han superado con éxito.

Paso 5: Usar Fixtures en pytest

Las fuciones fixture son utilizadas para proporcionar datos o recursos a las pruebas. Pueden utilizarse para configurar y desconfigurar entornos de prueba, cargar datos o realizar otras tareas de configuración. En pytest, las fuciones fixture se definen usando el decorador @pytest.fixture.

Aquí es cómo mejorar la fixture existente. Actualice la fixture del cliente para utilizar la lógica de configuración y desconfiguración:

test_app.py
@pytest.fixture
def client():
    """Set up a test client for the app with setup and teardown logic."""
    print("\nSetting up the test client")
    with app.test_client() as client:
        yield client  # Aquí es donde ocurre la prueba
    print("Tearing down the test client")

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Este ajuste agrega declaraciones de impresión para demostrar las fases de configuración y desconfiguración en la salida de pruebas. Estas pueden ser reemplazadas con código de gestión de recursos reales si es necesario.

Vamos a intentar ejecutar las pruebas de nuevo:

root@ubuntu:~# pytest -vs

La opción -v aumenta la verbosidad, y la opción -s permite que las declaraciones de impresión se muestren en la salida de la consola.

Debería ver la siguiente salida:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 5 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client ============================================ 5 passed in 0.35s =============================================

Paso 6: Agregar un Caso de Prueba de Falla

Vamos a agregar un caso de prueba de falla al archivo de pruebas existente. Modifique el archivo test_app.py y agregue la función de abajo al final para un caso de prueba de falla con un resultado incorrecto:

test_app.py
def test_multiply_edge_cases(client):
    """Test the multiply route with edge cases to demonstrate failing tests."""
    # Prueba con cero
    response = client.get('/multiply/0/5')
    assert response.status_code == 200
    assert response.json == {"result": 0}

    # Prueba con números grandes (esto podría fallar si no se maneja correctamente)
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

    # Prueba intencional de falla: resultado incorrecto
    response = client.get('/multiply/2/3')
    assert response.status_code == 200
    assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case"

Vamos a desglosar la función test_multiply_edge_cases y explicar qué hace cada parte:

  1. Prueba con cero:
    Esta prueba verifica si la función de multiplicación maneja correctamente la multiplicación por cero. Esperamos que el resultado sea 0 cuando se multiplica cualquier número por cero. Esto es un caso fronterizo importante que hay que probar porque algunas implementaciones podrían tener problemas con la multiplicación por cero.

  2. Prueba con números grandes:
    Esta prueba verifica si la función de multiplicación puede manejar números grandes sin sobrepasar el límite o tener problemas de precisión. Estamos multiplicando dos valores de un millón, esperando un resultado de un billón. Esta prueba es crucial porque verifica los límites superiores de la capacidad de la función. Tenga en cuenta que esto podría fallar si la implementación del servidor no maneja números grandes correctamente, lo que podría indicar la necesidad de bibliotecas de números grandes o un tipo de dato diferente.

  3. Prueba intencionalmente fallida:
    Esta prueba está deliberadamente configurada para fallar. Comprueba si 2 * 3 es igual a 7, lo cual es incorrecto. Esta prueba tiene como objetivo demostrar cómo se ve una prueba fallida en la salida de pruebas. Esto ayuda en la comprensión de cómo identificar y depurar pruebas fallidas, que es una habilidad fundamental en el desarrollo dirigido por pruebas y en los procesos de depuración.

Al incluir estos casos extremos y un fallo intencional, estás probando no solo la funcionalidad básica de tu ruta de multiplicación, sino también su comportamiento bajo condiciones extremas y sus capacidades de reporte de errores. Esta aproximación a las pruebas ayuda a garantizar la robustez y la confiabilidad de nuestra aplicación.

Vamos a intentar ejecutar las pruebas de nuevo:

root@ubuntu:~# pytest -vs

Deberías ver la siguiente salida:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 6 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_edge_cases Setting up the test client FAILED Tearing down the test client ================================================================= FAILURES ================================================================== _________________________________________________________ test_multiply_edge_cases __________________________________________________________ client = <FlaskClient <Flask 'app'>> def test_multiply_edge_cases(client): """Test the multiply route with edge cases to demonstrate failing tests.""" # Prueba con cero response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # Prueba con números grandes (esto podría fallar si no se maneja correctamente) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # Prueba fallida intencional: resultado incorrecto response = client.get('/multiply/2/3') assert response.status_code == 200 > assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case" E AssertionError: This test should fail to demonstrate a failing case E assert {'result': 6} == {'result': 7} E E Differing items: E {'result': 6} != {'result': 7} E E Full diff: E { E - 'result': 7,... E E ...Full output truncated (4 lines hidden), use '-vv' to show tests/test_app.py:61: AssertionError ========================================================== short test summary info ========================================================== FAILED tests/test_app.py::test_multiply_edge_cases - AssertionError: This test should fail to demonstrate a failing case ======================================================== 1 failed, 5 passed in 0.32s ========================================================

La mensaje de fallo de arriba indica que la prueba test_multiply_edge_cases en el archivo tests/test_app.py falló. específicamente, la última aserción en esta función de prueba causó el fallo.

Este fallo intencional es útil para demostrar cómo se informan las fallas en las pruebas y qué información se proporciona en el mensaje de fallo. Muestra la línea exacta donde ocurrió el fallo, los valores esperados y los valores reales, y la diferencia entre ambos.

En un escenario real, Ud. corregiría el código para que la prueba pasara o ajustaría la prueba si el resultado esperado era incorrecto. Sin embargo, en este caso, la falla es intencional por propósitos educativos.

Conclusión

En este tutorial, cubrimos cómo configurar pruebas unitarias para una aplicación de Flask utilizando pytest, integrar fixture de pytest y demostrar cómo se ve una falla de prueba. Al seguir estos pasos, Ud. puede asegurar que sus aplicaciones de Flask son confiables y mantenibles, minimizando errores y mejorando la calidad del código.

Puede referirse a la documentación oficial de Flask y Pytest para aprender más.

Source:
https://www.digitalocean.com/community/tutorials/unit-test-in-flask