Тестирование приложений 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
client = flaskr.app.test_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)
...
В общем случае это менее полезно, поскольку на этом этапе вы можете напрямую начать использовать тестовый клиент.
Моделирование ресурсов и контекста
Changelog
Введено в версии 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)
Поддержание контекста
Changelog
Введено в версии 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 больше недоступен (поскольку вы пытаетесь его использовать вне фактического запроса).
Доступ к сессиям и их изменение
Changelog
Введено в версии 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={
'username': 'flask', '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.0.x/testing/