Тестирование приложений 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"