Тестирование приложений Flask

Flask предоставляет утилиты для тестирования приложения. В этой документации рассматриваются методы работы с различными частями приложения в тестах.

Мы будем использовать фреймворк pytest для настройки и запуска наших тестов.

$ pip install pytest

В tutorial рассказывается о том, как написать тесты для 100% покрытия примера приложения блога Flaskr. Подробное описание конкретных тестов для приложения смотрите в the tutorial on tests.

Идентификационные тесты

Тесты обычно располагаются в папке tests. Тесты - это функции, которые начинаются с test_, в модулях Python, которые начинаются с test_. Тесты также могут быть сгруппированы в классы, которые начинаются с Test.

Бывает трудно понять, что тестировать. Как правило, старайтесь тестировать код, который вы пишете, а не код библиотек, которые вы используете, поскольку они уже протестированы. Старайтесь выделять сложное поведение в отдельные функции для тестирования по отдельности.

Приспособления

Фикстуры Pytest fixtures позволяют писать фрагменты кода, которые можно использовать многократно в разных тестах. Простой фикстур возвращает значение, но фикстур может также выполнять установку, выдавать значение, а затем завершать работу. Ниже показаны фикстуры для приложения, тестового клиента и CLI runner, которые можно поместить в tests/conftest.py.

Если вы используете application factory, определите приспособление app для создания и настройки экземпляра приложения. Вы можете добавить код до и после yield для установки и удаления других ресурсов, таких как создание и очистка базы данных.

Если вы не используете фабрику, у вас уже есть объект приложения, который вы можете импортировать и настроить напрямую. Вы все еще можете использовать приспособление app для установки и удаления ресурсов.

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()

Отправка запросов с помощью тестового клиента

Тестовый клиент выполняет запросы к приложению без запуска живого сервера. Клиент Flask расширяет Werkzeug’s client, дополнительную информацию смотрите в документации.

В client есть методы, соответствующие распространенным методам HTTP-запросов, таким как client.get() и client.post(). Они принимают множество аргументов для построения запроса; полную документацию вы можете найти в EnvironBuilder. Обычно вы используете path, query_string, headers и data или json.

Чтобы сделать запрос, вызовите метод, который должен использовать запрос, с указанием пути к тестируемому маршруту. Для изучения данных ответа возвращается объект TestResponse. Он имеет все обычные свойства объекта ответа. Обычно вы будете смотреть на response.data, который представляет собой байты, возвращенные представлением. Если вы хотите использовать текст, Werkzeug 2.1 предоставляет response.text, или используйте response.get_data(as_text=True).

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

Передайте дикту query_string={"key": "value", ...} для задания аргументов в строке запроса (после ? в URL). Передайте дикту headers={} для установки заголовков запроса.

Чтобы отправить тело запроса в запросе POST или PUT, передайте значение в data. Если передаются необработанные байты, то используется именно это тело. Обычно вы передаете dict для задания данных формы.

Данные формы

Чтобы отправить данные формы, передайте dict в data. Заголовок Content-Type будет автоматически установлен в multipart/form-data или application/x-www-form-urlencoded.

Если значение является файловым объектом, открытым для чтения байтов (режим "rb"), оно будет рассматриваться как загруженный файл. Чтобы изменить обнаруженное имя файла и тип содержимого, передайте кортеж (file, filename, content_type). Файловые объекты будут закрыты после выполнения запроса, поэтому для них не нужно использовать обычный шаблон with open() as f:.

Может быть полезно хранить файлы в папке tests/resources, а затем использовать pathlib.Path для получения файлов относительно текущего тестового файла.

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

JSON-данные

Чтобы отправить данные в формате JSON, передайте объект в json. Заголовок Content-Type будет установлен в application/json автоматически.

Аналогично, если ответ содержит данные JSON, атрибут response.json будет содержать десериализованный объект.

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"

Следующие перенаправления

По умолчанию клиент не делает дополнительных запросов, если ответ является перенаправлением. Если передать follow_redirects=True в метод запроса, клиент будет продолжать делать запросы до тех пор, пока не будет возвращен ответ без перенаправления.

TestResponse.history - это кортеж ответов, которые привели к окончательному ответу. Каждый ответ имеет атрибут request, который записывает запрос, породивший этот ответ.

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"

Доступ и изменение сеанса

Для доступа к контекстным переменным Flask, в основном session, используйте клиент в операторе with. Приложение и контекст запроса останутся активными после выполнения запроса, пока не закончится блок with.

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

Если вы хотите получить доступ или установить значение в сессии до выполнения запроса, используйте метод клиента session_transaction() в операторе with. Он возвращает объект сессии и сохранит сессию после завершения блока.

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"

Выполнение команд с помощью программы CLI Runner

Flask предоставляет test_cli_runner() для создания FlaskCliRunner, который выполняет команды CLI в изоляции и фиксирует вывод в объекте Result. Бегунок Flask расширяет Click’s runner, дополнительную информацию см. в документации.

Используйте метод бегуна invoke() для вызова команд так же, как они были бы вызваны с помощью команды flask из командной строки.

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(args="hello")
    assert "World" in result.output

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

Тесты, которые зависят от активного контекста

У вас могут быть функции, вызываемые из представлений или команд, которые ожидают активного application context или request context, поскольку обращаются к request, session или current_app. Вместо того чтобы проверять их, делая запрос или вызывая команду, вы можете создать и активировать контекст напрямую.

Используйте with app.app_context() для перехода к контексту приложения. Например, расширения базы данных обычно требуют активного контекста приложения для выполнения запросов.

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

Используйте with app.test_request_context() для передачи контекста запроса. Он принимает те же аргументы, что и методы запроса тестового клиента.

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."

Создание контекста тестового запроса не запускает никакого кода диспетчеризации Flask, поэтому функции before_request не вызываются. Если вам нужно вызвать эти функции, обычно лучше сделать полный запрос. Однако их можно вызвать и вручную.

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"
Вернуться на верх