Тестирование приложений Flask
Непроверенное — сломанное.
Происхождение этой цитаты неизвестно, и хотя она не совсем верна, она также не сильно отличается от истины. Непроверенные приложения затрудняют улучшение существующего кода, и разработчики непроверенных приложений имеют тенденцию к довольно сильной паранойе. Если у приложения есть автоматические тесты, вы можете безопасно вносить изменения и мгновенно узнать, не сломалось ли что-нибудь.
Flask предоставляет способ тестирования вашего приложения, предоставляя доступ к тесту Werkzeug Client и обрабатывая локальные переменные контекста для вас. Затем вы можете использовать это со своим любимым решением для тестирования.
В этом руководстве мы будем использовать пакет pytest в качестве базовой структуры для наших тестов. Вы можете установить его с помощью pip, как показано ниже:
$ pip install pytest
Приложение
Сначала нам нужно приложение для тестирования; мы будем использовать приложение из учебника. Если у вас еще нет этого приложения, получите исходный код из примеров.
Для того, чтобы мы могли правильно импортировать модуль flaskr, нам нужно запустить pip install -e . в папке tutorial.
Структура тестов
Мы начинаем с добавления каталога tests в корень приложения. Затем создаем Python-файл для хранения наших тестов (test_flaskr.py). Если мы отформатируем имя файла как test_*.py, pytest автоматически обнаружит его.
Далее, мы создаем фиксатуру pytest под названием client(), которая настраивает приложение для тестирования и инициализирует новую базу данных:
import os
import tempfile
import pytest
from flaskr import create_app
@pytest.fixture
def client():
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True
with flaskr.app.test_client() as client:
with flaskr.app.app_context():
flaskr.init_db()
yield client
os.close(db_fd)
os.unlink(flaskr.app.config['DATABASE'])
Эта фиксатура client будет вызываться каждым отдельным тестом. Она предоставляет нам простой интерфейс к приложению, где мы можем запускать запросы тестов к приложению. Клиент также будет отслеживать куки за нас.
Во время настройки флаг конфигурации TESTING активируется. Это отключает перехват ошибок во время обработки запросов, чтобы получить более подробные сообщения об ошибках при выполнении запросов тестов к приложению.
Поскольку SQLite3 основан на файловой системе, мы можем легко использовать модуль tempfile, чтобы создать временную базу данных и инициализировать её. Функция mkstemp() делает за нас две вещи: возвращает низкоуровневый дескриптор файла и случайное имя файла, которое мы используем в качестве имени базы данных. Нам просто нужно сохранить db_fd, чтобы мы могли использовать функцию os.close() для закрытия файла.
Для удаления базы данных после теста фиксатура закрывает файл и удаляет его из файловой системы.
Если мы сейчас запустим набор тестов, мы должны увидеть следующий вывод:
$ pytest ================ test session starts ================ rootdir: ./flask/examples/flaskr, inifile: setup.cfg collected 0 items =========== no tests ran in 0.07 seconds ============
Несмотря на то, что он не запускал никаких реальных тестов, мы уже знаем, что наше приложение flaskr синтаксически верно, иначе импорт завершился бы исключением.
Первый тест
Теперь пришло время начать тестирование функциональности приложения. Давайте проверим, отображает ли приложение «Пока нет записей» при доступе к корню приложения (/). Для этого мы добавим новую функцию теста в test_flaskr.py, как показано ниже:
def test_empty_db(client):
"""Start with a blank database."""
rv = client.get('/')
assert b'No entries here so far' in rv.data
Обратите внимание, что наши функции тестов начинаются со слова test; это позволяет pytest автоматически определять функцию как тест для запуска.
Используя client.get, мы можем отправить HTTP GET запрос к приложению с указанным путем. Результатом будет объект response_class. Теперь мы можем использовать атрибут data, чтобы проверить возвращаемое значение (как строку) из приложения. В этом случае мы убеждаемся, что 'No entries here so far' является частью вывода.
Запустите его снова, и вы должны увидеть один пройденный тест:
$ pytest -v ================ test session starts ================ rootdir: ./flask/examples/flaskr, inifile: setup.cfg collected 1 items tests/test_flaskr.py::test_empty_db PASSED ============= 1 passed in 0.10 seconds ==============
Вход и выход
Большая часть функциональности нашего приложения доступна только для административного пользователя, поэтому нам нужен способ входа и выхода нашего тестового клиента из приложения. Для этого мы отправляем несколько запросов на страницы входа и выхода с необходимыми данными формы (имя пользователя и пароль). И поскольку страницы входа и выхода перенаправляют, мы говорим клиенту follow_redirects.
Добавьте следующие две функции в свой файл test_flaskr.py:
def login(client, username, password):
return client.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(client):
return client.get('/logout', follow_redirects=True)
Теперь мы можем легко проверить, что вход и выход работают, и что они не проходят с неверными учетными данными. Добавьте эту новую функцию теста:
def test_login_logout(client):
"""Make sure login and logout works."""
username = flaskr.app.config["USERNAME"]
password = flaskr.app.config["PASSWORD"]
rv = login(client, username, password)
assert b'You were logged in' in rv.data
rv = logout(client)
assert b'You were logged out' in rv.data
rv = login(client, f"{username}x", password)
assert b'Invalid username' in rv.data
rv = login(client, username, f'{password}x')
assert b'Invalid password' in rv.data
Тест добавления сообщений
Мы также должны проверить, что добавление сообщений работает. Добавьте новую функцию теста, как показано ниже:
def test_messages(client):
"""Test that messages work."""
login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
rv = client.post('/add', data=dict(
title='<Hello>',
text='<strong>HTML</strong> allowed here'
), follow_redirects=True)
assert b'No entries here so far' not in rv.data
assert b'<Hello>' in rv.data
assert b'<strong>HTML</strong> allowed here' in rv.data
Здесь мы проверяем, что HTML разрешен в тексте, но не в заголовке, что является ожидаемым поведением.
Запуск этого теста теперь должен дать нам три пройденных теста:
$ pytest -v ================ test session starts ================ rootdir: ./flask/examples/flaskr, inifile: setup.cfg collected 3 items tests/test_flaskr.py::test_empty_db PASSED tests/test_flaskr.py::test_login_logout PASSED tests/test_flaskr.py::test_messages PASSED ============= 3 passed in 0.23 seconds ==============
Другие трюки тестирования
Помимо использования тестового клиента, как показано выше, также есть метод test_request_context(), который может быть использован в сочетании с оператором with, чтобы временно активировать контекст запроса. С этим вы можете получить доступ к объектам request, g и session, как и в функциях представлений. Вот полный пример, который демонстрирует этот подход:
import flask
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
Все остальные связанные с контекстом объекты могут быть использованы таким же образом.
Если вы хотите протестировать свое приложение с различными конфигурациями, и не кажется, что есть хороший способ сделать это, рассмотрите возможность переключения на фабрики приложений (см. Фабрики приложений).
Однако имейте в виду, что если вы используете контекст запроса теста, функции before_request() и after_request() не вызываются автоматически. Однако функции teardown_request() действительно выполняются, когда контекст запроса теста выходит из блока with. Если вы хотите, чтобы функции before_request() также вызывались, вам необходимо вызвать preprocess_request() самостоятельно:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
app.preprocess_request()
...
Это может быть необходимо для открытия подключений к базам данных или для чего-то подобного, в зависимости от того, как было спроектировано ваше приложение.
Если вы хотите вызвать функции after_request(), вам необходимо обратиться к process_response(), но для этого вам необходимо передать ему объект ответа:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
resp = Response('...')
resp = app.process_response(resp)
...
Это в целом менее полезно, потому что в этот момент вы можете напрямую начать использование тестового клиента.
Имитация ресурсов и контекста
Журнал изменений
Введено в версии 0.10.
Очень распространенным шаблоном является хранение информации об авторизации пользователя и подключений к базам данных в контексте приложения или объекте flask.g. Общий шаблон для этого — поместить объект туда при первом использовании и затем удалить его при разборе. Например, представьте себе такой код для получения текущего пользователя:
def get_user():
user = getattr(g, 'user', None)
if user is None:
user = fetch_current_user_from_database()
g.user = user
return user
Для теста было бы неплохо переопределить этого пользователя извне, не изменяя какой-либо код. Этого можно добиться, подключив сигнал flask.appcontext_pushed:
from contextlib import contextmanager
from flask import appcontext_pushed, g
@contextmanager
def user_set(app, user):
def handler(sender, **kwargs):
g.user = user
with appcontext_pushed.connected_to(handler, app):
yield
И затем использовать его:
from flask import json, jsonify
@app.route('/users/me')
def users_me():
return jsonify(username=g.user.username)
with user_set(app, my_user):
with app.test_client() as c:
resp = c.get('/users/me')
data = json.loads(resp.data)
assert data['username'] == my_user.username
Сохранение контекста
Журнал изменений
Введено в версии 0.4.
Иногда полезно инициировать обычный запрос, но сохранить контекст на немного большее время, чтобы можно было провести дополнительный анализ. В Flask 0.4 это возможно, используя test_client() с блоком with:
app = flask.Flask(__name__)
with app.test_client() as c:
rv = c.get('/?tequila=42')
assert request.args['tequila'] == '42'
Если вы будете использовать только test_client() без блока with, assert завершится ошибкой, потому что request больше недоступен (потому что вы пытаетесь использовать его за пределами фактического запроса).
Доступ к сессиям и их изменение
Журнал изменений
Введено в версии 0.8.
Иногда может быть очень полезно получить доступ к сессиям или изменить их с помощью тестового клиента. В целом есть два способа сделать это. Если вы просто хотите убедиться, что сессия имеет определенные ключи, установленные на определенные значения, вы можете просто сохранить контекст и получить доступ к flask.session:
with app.test_client() as c:
rv = c.get('/')
assert flask.session['foo'] == 42
Однако это не позволяет также изменять сеанс или получать доступ к сеансу до отправки запроса. Начиная с Flask 0.8, мы предоставляем так называемую «транзакцию сеанса», которая имитирует соответствующие вызовы для открытия сеанса в контексте тестового клиента и его изменения. В конце транзакции сеанс сохраняется и готов к использованию тестовым клиентом. Это работает независимо от используемого бэкенда сеанса:
with app.test_client() as c:
with c.session_transaction() as sess:
sess['a_key'] = 'a value'
# once this is reached the session was stored and ready to be used by the client
c.get(...)
Обратите внимание, что в этом случае вам необходимо использовать объект sess вместо прокси flask.session. Однако сам объект предоставит тот же интерфейс.
Тестирование JSON-API
Журнал изменений
Новое в версии 1.0.
Flask предоставляет отличную поддержку JSON и является популярным выбором для создания JSON-API. Отправка запросов с данными JSON и проверка данных JSON в ответах очень удобна:
from flask import request, jsonify
@app.route('/api/auth')
def auth():
json_data = request.get_json()
email = json_data['email']
password = json_data['password']
return jsonify(token=generate_token(email, password))
with app.test_client() as c:
rv = c.post('/api/auth', json={
'email': 'flask@example.com', 'password': 'secret'
})
json_data = rv.get_json()
assert verify_token(email, json_data['token'])
Передача аргумента json в методах тестового клиента устанавливает данные запроса в сериализованный в JSON объект и задаёт тип содержимого на application/json. Данные JSON можно получить из запроса или ответа с помощью get_json.
Тестирование команд CLI
Click поставляется с утилитами для тестирования ваших команд CLI. CliRunner выполняет команды изолированно и сохраняет вывод в объекте Result.
Flask предоставляет test_cli_runner() для создания FlaskCliRunner, который автоматически передаёт приложение Flask в CLI. Используйте метод invoke() для вызова команд таким же образом, как они вызываются из командной строки.
import click
@app.cli.command('hello')
@click.option('--name', default='World')
def hello_command(name):
click.echo(f'Hello, {name}!')
def test_hello():
runner = app.test_cli_runner()
# invoke the command directly
result = runner.invoke(hello_command, ['--name', 'Flask'])
assert 'Hello, Flask' in result.output
# or by name
result = runner.invoke(args=['hello'])
assert 'World' in result.output
В приведённом выше примере вызов команды по имени полезен, потому что он проверяет, что команда была правильно зарегистрирована в приложении.
Если вы хотите протестировать, как ваша команда обрабатывает параметры, не выполняя команду, используйте метод make_context(). Это полезно для тестирования сложных правил валидации и пользовательских типов.
def upper(ctx, param, value):
if value is not None:
return value.upper()
@app.cli.command('hello')
@click.option('--name', default='World', callback=upper)
def hello_command(name):
click.echo(f'Hello, {name}!')
def test_hello_params():
context = hello_command.make_context('hello', ['--name', 'flask'])
assert context.params['name'] == 'FLASK'
© 2007–2021 Pallets
Licensed under the BSD 3-clause License.
https://flask.palletsprojects.com/en/2.0.x/testing/