Фреймворк 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"