Тестирование приложений Flask¶
То, что не протестировано, не работает.
Оригинал цитаты неизвестен, и хотя она не совсем правильная, всё же она недалека от истины. Не подвергнутые тестированию приложения затрудняют развитие существующего кода, а разработчики не протестированных приложений, как правило, подвергаются влиянию паранойи. Если у приложения есть автоматизированные тесты, можно безопасно вносить в него изменения и узнавать сразу, если что-то пошло не так.
Flask предоставляет вам способ тестирования вашего приложения с помощью теста из
состава Werkzeug Client
и через обработку локальных
переменных контекста. В дальнейшем вы можете это использовать с вашим любимым
средством тестирования. В данной документации мы будем использовать идущий в
комплекте с Python пакет unittest
.
Приложение¶
Для начала, нам нужно иметь приложение для тестирования; мы будем использовать приложение из Учебник. Если у вас пока нет этого приложения, возьмите его исходные тексты из примеров.
Скелет для тестирования¶
Для того, чтобы протестировать приложение, мы добавим второй модуль (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.config['TESTING'] = True
self.app = flaskr.app.test_client()
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
. Это приводит к отключению
отлова ошибок во время обработки запроса, и вы получаете более качественные
отчёты об ошибках при выполнении по отношению к приложению тестовых запросов.
Этот тестовый клиент даст нам простой интерфейс к приложению. Мы можем запускать тестовые запросы к приложению, а клиент будет также отслеживать для нас cookies.
Так как 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()
self.app = flaskr.app.test_client()
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 'Unbelievable. No entries here so far' in rv.data
Заметим, что наша тестовая функция начинается со слова test; это позволяет
модулю unittest
определить метод как тест для запуска.
С помощью использования self.app.get мы можем послать приложению HTTP-запрос
GET с заданным путём. Возвращаемым значением будет объект
response_class
. Теперь мы можем для проверки возвращаемого
значения (как строки) использовать атрибут
data
. В данном случае, мы хотим
убедиться в том, что в выводе присутствует 'Unbelievable. 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 'You were logged in' in rv.data
rv = self.logout()
assert 'You were logged out' in rv.data
rv = self.login('adminx', 'default')
assert 'Invalid username' in rv.data
rv = self.login('admin', 'defaultx')
assert '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 'No entries here so far' not in rv.data
assert '<Hello>' in rv.data
assert '<strong>HTML</strong> allowed here' in rv.data
Здесь мы проверяем, что запись соответсвует установленным правилам - HTML может присутствовать в тексте, но не заголовке.
Теперь при запуске мы должны увидеть, что пройдено 3 теста:
$ python flaskr_tests.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.332s
OK
Чтобы посмотреть более сложные тесты заголовков и статусных кодов, обратитесь к исходникам Flask MiniTwit Example, в которых есть более развёрнутый набор тестов.
Другие трюки с тестами¶
Кроме вышеуказанного использования клиента тестирования, есть ещё
метод test_request_context()
, который может быть
использован в комбинации с оператором with для временной активации
контекста запроса. С помощью него вы можете получить доступ к
объектам request
, g
и
session
, подобно функции представления. Вот
полный пример, который демонстрирует подобный подход:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
Подобным способом могут использоваться и все другие объекты, связанные с контекстом.
Если вы хотите протестировать ваше приложение в другой конфигурации, а хорошего способа, чтобы это сделать, нет, подумайте над переходом на фабрики приложений (см. app-factories).
Заметим, однако, что если вы используете контекст тестового запроса,
функции 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
Для его тестирования будет неплохо переопределить пользователя извне, без
изменений в коде. Это можно сделать тривиально, если зацепить (hooking)
сигнал flask.appcontext_pushed
:
from contextlib import contextmanager
from flask import appcontext_pushed
@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'
# здесь сессия будет сохранена
Заметим, что в данном случае вам необходимо использовать вместо прокси
flask.session
объект sess
. Однако, объект сам по себе
обеспечит вас тем же интерфейсом.