Сообщить об ошибке.

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

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

Содержание:


Идентификация Тестов.

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

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

Фикстуры.

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

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

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

import pytest
from my_project import create_app

@pytest.fixture()
def app():
    app = create_app()
    app.config.update({
        "TESTING": True,
    })
    # здесь могут быть другие настройки 
    yield app
    # здесь нужно расположить код для очистки ресурсов 

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

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

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

Тестовый клиент отправляет запросы к приложению, не запуская работающий сервер. Клиент Flask расширяет клиент werkzeug.

У тестового клиента есть методы, соответствующие обычным методам HTTP-запросов, например client.get() и client.post(). Они принимают много аргументов для построения запроса. Можно найти полную документацию в EnvironBuilder. Часто используются некоторые из них: path, query, 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

Чтобы установить параметры в строке запроса (после ? в URL-адресе), необходимо передать словарь аргументу тестового клиента query={'key': 'value', ...}. Чтобы установить заголовки запроса, необходимо передать словарь аргументу headers={} тестового клиента.

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

Тестирование данных, передаваемых формой.

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

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

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

from pathlib import Path

# получаем папку ресурсов в папке тестов
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")
    # Проверяет, что на перенаправление был один ответ.
    assert len(response.history) == 1
    # Проверяет, что второй запрос был направлен на страницу "/index"
    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"})
        # сеанс по-прежнему доступен
        assert session["user_id"] == 1

    # сеанс больше не доступен

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

from flask import session

def test_modify_session(client):
    with client.session_transaction() as session:
        # установим идентификатор пользователя, 
        # не проходя через маршрут входа в систему
        session["user_id"] = 1

    # теперь сеанс сохранен

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

Запуск команд с помощью CLI Runner

Flask предоставляет test_cli_runner() для создания FlaskCliRunner, который запускает команды CLI изолированно и фиксирует вывод в объекте Result. Примечание. CLI Runner Flask расширяет CLI Runner модуля click.

Используйте метод FlaskCliRunner.invoke() CLI Runner для вызова команд так же, как они вызываются командой 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

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

Могут быть функции, вызываемые из представлений или команд, которые ожидают активного контекста приложения или контекста запроса, т.к. они обращаются к запросу 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": ""}
    ):
        # вызов функции, которая обращается к `request`
        messages = validate_edit_user()

    assert messages["name"][0] == "Имя не может быть пустым"

Создание контекста тестового запроса не запускает какой-либо код диспетчеризации 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"