Prueba de aplicaciones Flask

Flask proporciona utilidades para probar una aplicación. Esta documentación repasa las técnicas para trabajar con diferentes partes de la aplicación en las pruebas.

Utilizaremos el framework pytest para configurar y ejecutar nuestras pruebas.

$ pip install pytest

El tutorial repasa cómo escribir pruebas para una cobertura del 100% de la aplicación de ejemplo del blog Flaskr. Consulta el the tutorial on tests para una explicación detallada de pruebas específicas para una aplicación.

Identificación de pruebas

Las pruebas se encuentran normalmente en la carpeta tests. Las pruebas son funciones que empiezan por test_, en módulos de Python que empiezan por test_. Las pruebas también se pueden agrupar en clases que empiezan por Test.

Puede ser difícil saber qué probar. En general, intenta probar el código que escribes, no el de las bibliotecas que utilizas, ya que éstas ya están probadas. Intenta extraer los comportamientos complejos como funciones separadas para probarlas individualmente.

Fixtures

Los fixtures de Pytest permiten escribir piezas de código que son reutilizables en todas las pruebas. Un fixture simple devuelve un valor, pero un fixture también puede hacer la configuración, producir un valor, y luego hacer el desmontaje. Los fixture para la aplicación, el cliente de pruebas y el ejecutor CLI se muestran a continuación, y pueden colocarse en tests/conftest.py.

Si utilizas una application factory, define un fixture app para crear y configurar una instancia de la aplicación. Puedes añadir código antes y después del yield para configurar y desmontar otros recursos, como crear y borrar una base de datos.

Si no estás usando una fábrica, ya tienes un objeto de aplicación que puedes importar y configurar directamente. Todavía puedes usar un fixture de app para configurar y desmontar recursos.

import pytest
from my_project import create_app

@pytest.fixture()
def app():
    app = create_app()
    app.config.update({
        "TESTING": True,
    })

    # other setup can go here

    yield app

    # clean up / reset resources here


@pytest.fixture()
def client(app):
    return app.test_client()


@pytest.fixture()
def runner(app):
    return app.test_cli_runner()

Envío de solicitudes con el cliente de prueba

El cliente de prueba hace peticiones a la aplicación sin ejecutar un servidor en vivo. El cliente de Flask extiende Werkzeug’s client, vea esos docs para información adicional.

El client tiene métodos que coinciden con los métodos comunes de petición HTTP, como client.get() y client.post(). Toman muchos argumentos para construir la petición; puedes encontrar la documentación completa en EnvironBuilder. Normalmente usarás path, query_string, headers, y data o json.

Para realizar una petición, se llama al método que debe utilizar la petición con la ruta a probar. Se devuelve una TestResponse para examinar los datos de la respuesta. Tiene todas las propiedades habituales de un objeto respuesta. Por lo general, se examinará response.data, que son los bytes devueltos por la vista. Si quieres usar texto, Werkzeug 2.1 proporciona response.text, o usa response.get_data(as_text=True).

def test_request_example(client):
    response = client.get("/posts")
    assert b"<h2>Hello, World!</h2>" in response.data

Pasa un dict query_string={"key": "valor", ...} para establecer argumentos en la cadena de consulta (después del ? en la URL). También puedes pasar una cadena si quieres establecer un valor específico directamente.

Pasa un dict a headers={} para establecer las cabeceras de las peticiones.

Para enviar un cuerpo de petición en una petición POST o PUT, pase un valor a data. Si se pasan bytes sin procesar, se utiliza ese cuerpo exacto. Normalmente, se pasa un dict para establecer los datos del formulario.

Datos del formulario

Para enviar los datos del formulario, pasa un dict a data. La cabecera Content-Type se establecerá como multipart/form-data o application/x-www-form-urlencoded automáticamente.

Si un valor es un objeto de archivo abierto para la lectura de bytes (modo "rb"), será tratado como un archivo cargado. Para cambiar el nombre de archivo y el tipo de contenido detectados, pase una tupla (file, filename, content_type). Los objetos archivo se cerrarán después de realizar la petición, por lo que no es necesario utilizar el patrón habitual with open() as f:.

Puede ser útil almacenar los archivos en una carpeta tests/resources, y luego utilizar pathlib.Path para obtener los archivos relativos al archivo de prueba actual.

from pathlib import Path

# get the resources folder in the tests folder
resources = Path(__file__).parent / "resources"

def test_edit_user(client):
    response = client.post("/user/2/edit", data={
        "name": "Flask",
        "theme": "dark",
        "picture": (resources / "picture.png").open("rb"),
    })
    assert response.status_code == 200

Datos JSON

Para enviar datos JSON, pasa un objeto a json. La cabecera Content-Type se establecerá como application/json automáticamente.

Del mismo modo, si la respuesta contiene datos JSON, el atributo response.json contendrá el objeto deserializado.

def test_json_data(client):
    response = client.post("/graphql", json={
        "query": """
            query User($id: String!) {
                user(id: $id) {
                    name
                    theme
                    picture_url
                }
            }
        """,
        variables={"id": 2},
    })
    assert response.json["data"]["user"]["name"] == "Flask"

Siguiendo las redirecciones

Por defecto, el cliente no realiza peticiones adicionales si la respuesta es una redirección. Al pasar follow_redirects=True a un método de petición, el cliente continuará haciendo peticiones hasta que se devuelva una respuesta que no sea una redirección.

TestResponse.history es una tupla de las respuestas que condujeron a la respuesta final. Cada respuesta tiene un atributo request que registra la solicitud que produjo esa respuesta.

def test_logout_redirect(client):
    response = client.get("/logout")
    # Check that there was one redirect response.
    assert len(response.history) == 1
    # Check that the second request was to the index page.
    assert response.request.path == "/index"

Acceder y modificar la sesión

Para acceder a las variables de contexto de Flask, principalmente session, utiliza el cliente en una sentencia with. La aplicación y el contexto de la solicitud permanecerán activos después de hacer una solicitud, hasta que el bloque with termine.

from flask import session

def test_access_session(client):
    with client:
        client.post("/auth/login", data={"username": "flask"})
        # session is still accessible
        assert session["user_id"] == 1

    # session is no longer accessible

Si quieres acceder o establecer un valor en la sesión antes de hacer una petición, utiliza el método session_transaction() del cliente en una sentencia with. Devuelve un objeto de sesión, y guardará la sesión una vez que el bloque termine.

from flask import session

def test_modify_session(client):
    with client.session_transaction() as session:
        # set a user id without going through the login route
        session["user_id"] = 1

    # session is saved now

    response = client.get("/users/me")
    assert response.json["username"] == "flask"

Ejecución de comandos con el CLI Runner

Flask proporciona test_cli_runner() para crear un FlaskCliRunner, que ejecuta comandos CLI de forma aislada y captura la salida en un objeto Result. El corredor de Flask extiende el Cliclic’s runner, vea esos docs para información adicional.

Utiliza el método invoke() del corredor para llamar a los comandos de la misma manera que se llamarían con el comando flask desde la línea de comandos.

import click

@app.cli.command("hello")
@click.option("--name", default="World")
def hello_command(name):
    click.echo(f"Hello, {name}!")

def test_hello_command(runner):
    result = runner.invoke(["hello"])
    assert "World" in result.output

    result = runner.invoke(["hello", "--name", "Flask"])
    assert "Flask" in result.output

Pruebas que dependen de un Contexto Activo

Puedes tener funciones que son llamadas desde vistas o comandos, que esperan un contexto activo de application context o request context porque acceden a request, session, o current_app. En lugar de probarlos haciendo una petición o invocando el comando, puedes crear y activar un contexto directamente.

Utiliza with app.app_context() para impulsar un contexto de aplicación. Por ejemplo, las extensiones de bases de datos suelen requerir un contexto de aplicación activo para realizar consultas.

def test_db_post_model(app):
    with app.app_context():
        post = db.session.query(Post).get(1)

Utiliza with app.app_context() para impulsar un contexto de aplicación. Por ejemplo, las extensiones de bases de datos suelen requerir un contexto de aplicación activo para realizar consultas.

def test_validate_user_edit(app):
    with app.test_request_context(
        "/user/2/edit", method="POST", data={"name": ""}
    ):
        # call a function that accesses `request`
        messages = validate_edit_user()

    assert messages["name"][0] == "Name cannot be empty."

La creación de un contexto de petición de prueba no ejecuta ningún código de envío de Flask, por lo que no se llaman las funciones before_request. Si necesitas llamarlas, normalmente es mejor hacer una petición completa. Sin embargo, es posible llamarlas manualmente.

def test_auth_token(app):
    with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}):
        app.preprocess_request()
        assert g.user.name == "Flask"