Тестирование Flask-приложений
Непроверенное — сломанное.
Происхождение этой цитаты неизвестно, и хотя она не полностью верна, она также не сильно отличается от истины. Непроверенные приложения затрудняют улучшение существующего кода, и разработчики непроверенных приложений склонны к чрезмерной паранойе. Если приложение имеет автоматические тесты, вы можете безопасно вносить изменения и мгновенно узнать, не сломалось ли что-нибудь.
Flask предоставляет способ тестирования вашего приложения, предоставляя доступ к инструменту тестирования Werkzeug Client и обрабатывая локальные переменные контекста за вас. Затем вы можете использовать это с вашим любимым решением для тестирования. В этом руководстве мы будем использовать пакет unittest, который предварительно установлен с Python.
Приложение
Сначала нам нужно приложение для тестирования; мы будем использовать приложение из Руководства. Если у вас еще нет этого приложения, получите исходные коды из примеров.
Каркас для тестирования
Для тестирования приложения мы добавим второй модуль (flaskr_tests.py) и создадим там каркас для unittest:
import os
import flaskr
import unittest
import tempfile
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.testing = True
self.app = flaskr.app.test_client()
with flaskr.app.app_context():
flaskr.init_db()
def tearDown(self):
os.close(self.db_fd)
os.unlink(flaskr.app.config['DATABASE'])
if __name__ == '__main__':
unittest.main()
Код в методе setUp() создаёт новый клиент для тестирования и инициализирует новую базу данных. Эта функция вызывается перед выполнением каждой отдельной тестовой функции. Чтобы удалить базу данных после теста, мы закрываем файл и удаляем его из файловой системы в методе tearDown(). Кроме того, во время настройки активируется флаг конфигурации TESTING. Его действие заключается в отключении обработки ошибок во время обработки запросов, чтобы получать более подробные сообщения об ошибках при выполнении тестовых запросов к приложению.
Этот тестовый клиент предоставит нам простой интерфейс к приложению. Мы можем вызывать тестовые запросы к приложению, а клиент также будет отслеживать куки за нас.
Поскольку SQLite3 основан на файловой системе, мы можем легко использовать модуль tempfile для создания временной базы данных и её инициализации. Функция mkstemp() выполняет две вещи за нас: она возвращает дескриптор файла низкого уровня и случайное имя файла, последнее мы используем в качестве имени базы данных. Нам просто нужно сохранить db_fd для использования функции os.close() для закрытия файла.
Если мы теперь запустим набор тестов, мы должны увидеть следующий вывод:
$ python flaskr_tests.py ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK
Несмотря на то, что он не выполнял никаких реальных тестов, мы уже знаем, что наше приложение flaskr синтаксически верно, иначе импорт завершился бы исключением.
Первый тест
Теперь пришло время начать тестирование функциональности приложения. Давайте проверим, отображает ли приложение «Пока здесь нет записей» при обращении к корню приложения (/). Для этого мы добавим новый тестовый метод в наш класс, вот так:
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.testing = True
self.app = flaskr.app.test_client()
with flaskr.app.app_context():
flaskr.init_db()
def tearDown(self):
os.close(self.db_fd)
os.unlink(flaskr.app.config['DATABASE'])
def test_empty_db(self):
rv = self.app.get('/')
assert b'No entries here so far' in rv.data
Обратите внимание, что наши тестовые функции начинаются со слова test; это позволяет unittest автоматически распознавать метод как тест для выполнения.
Используя self.app.get, мы можем отправить HTTP GET запрос к приложению с заданным путем. Возвращаемое значение будет объектом response_class. Теперь мы можем использовать атрибут data, чтобы проверить возвращаемое значение (как строку) из приложения. В этом случае мы убеждаемся, что 'No entries here so far' является частью вывода.
Запустите его снова, и вы должны увидеть один пройденный тест:
$ python flaskr_tests.py . ---------------------------------------------------------------------- Ran 1 test in 0.034s OK
Вход и выход
Большая часть функциональности нашего приложения доступна только административному пользователю, поэтому нам нужен способ входа и выхода тестового клиента в приложение. Для этого мы отправляем несколько запросов на страницы входа и выхода с необходимыми данными формы (имя пользователя и пароль). И поскольку страницы входа и выхода перенаправляют, мы говорим клиенту follow_redirects.
Добавьте следующие два метода в ваш класс FlaskrTestCase:
def login(self, username, password):
return self.app.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(self):
return self.app.get('/logout', follow_redirects=True)
Теперь мы можем легко проверить, что вход и выход работают, и что вход с неверными учетными данными терпит неудачу. Добавьте этот новый тест в класс:
def test_login_logout(self):
rv = self.login('admin', 'default')
assert b'You were logged in' in rv.data
rv = self.logout()
assert b'You were logged out' in rv.data
rv = self.login('adminx', 'default')
assert b'Invalid username' in rv.data
rv = self.login('admin', 'defaultx')
assert b'Invalid password' in rv.data
Тестирование добавления сообщений
Мы также должны проверить, что добавление сообщений работает. Добавьте новый тестовый метод, как это:
def test_messages(self):
self.login('admin', 'default')
rv = self.app.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 разрешен в тексте, но не в заголовке, что соответствует предполагаемому поведению.
Запуск этого теперь должен дать нам три пройденных теста:
$ python flaskr_tests.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.332s OK
Для более сложных тестов с заголовками и кодами состояния ознакомьтесь с примером MiniTwit из исходного кода, который содержит более обширный набор тестов.
Другие хитрости тестирования
Помимо использования тестового клиента, как показано выше, есть также метод 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
Обратите внимание, что в этом случае вам нужно использовать объект sess вместо прокси flask.session. Однако сам объект предоставит тот же интерфейс.
© 2007–2020 Pallets
Licensed under the BSD 3-clause License.
https://flask.palletsprojects.com/en/0.12.x/testing/