Тестирование приложений Flask
Непроверенное — сломанное.
Происхождение этой цитаты неизвестно, и хотя она не совсем верна, она также не сильно далека от истины. Непроверенные приложения затрудняют улучшение существующего кода, и разработчики непроверенных приложений склонны к довольно сильной паранойе. Если приложение имеет автоматизированные тесты, вы можете безопасно вносить изменения и мгновенно узнать, не сломалось ли что-нибудь.
Flask предоставляет способ тестирования вашего приложения, предоставляя доступ к тестовому клиенту Werkzeug Client и обрабатывая для вас контекстные локальные переменные. Затем вы можете использовать это с вашим любимым решением для тестирования.
В этой документации мы будем использовать пакет pytest в качестве базовой структуры для наших тестов. Вы можете установить его с помощью pip, как показано ниже:
$ pip install pytest
Приложение
Сначала нам нужно приложение для тестирования; мы будем использовать приложение из учебника. Если у вас еще нет этого приложения, получите исходный код из примеров.
Структура для тестирования
Мы начинаем с добавления каталога tests в корень приложения. Затем создайте файл Python для хранения наших тестов (test_flaskr.py). Если мы отформатируем имя файла как test_*.py, pytest автоматически его обнаружит.
Далее, мы создаем фиксатуру pytest с именем client() которая настраивает приложение для тестирования и инициализирует новую базу данных:
import os
import tempfile
import pytest
from flaskr import flaskr
@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'])
Эта фиксатура клиента будет вызываться каждым отдельным тестом. Она предоставляет нам простой интерфейс к приложению, где мы можем инициировать тестовые запросы к приложению. Клиент также будет отслеживать куки за нас.
Во время настройки флаг конфигурации 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."""
rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['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, flaskr.app.config['USERNAME'] + 'x', flaskr.app.config['PASSWORD'])
assert b'Invalid username' in rv.data
rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['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)
self.assert_equal(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–2020 Pallets
Licensed under the BSD 3-clause License.
https://flask.palletsprojects.com/en/1.1.x/testing/