Фреймворк 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
. Заголовок 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"
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"