.. _testing: Тестирование приложений Flask ============================= **То, что не протестировано, не работает.** Оригинал цитаты неизвестен, и хотя она не совсем правильная, всё же она недалека от истины. Не подвергнутые тестированию приложения затрудняют развитие существующего кода, а разработчики не протестированных приложений, как правило, подвергаются влиянию паранойи. Если у приложения есть автоматизированные тесты, можно безопасно вносить в него изменения и узнавать сразу, если что-то пошло не так. Flask предоставляет вам способ тестирования вашего приложения с помощью теста из состава Werkzeug :class:`~werkzeug.test.Client` и через обработку локальных переменных контекста. В дальнейшем вы можете это использовать с вашим любимым средством тестирования. В данной документации мы будем использовать идущий в комплекте с Python пакет :mod:`unittest`. Приложение ---------- Для начала, нам нужно иметь приложение для тестирования; мы будем использовать приложение из :ref:`tutorial`. Если у вас пока нет этого приложения, возьмите его исходные тексты из `примеров`_. .. _примеров: http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ Скелет для тестирования ----------------------- Для того, чтобы протестировать приложение, мы добавим второй модуль (`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() Код метода :meth:`~unittest.TestCase.setUp` создаёт нового клиента тестирования и инициализирует новую базу данных. Эта функция вызывается перед запуском каждой индивидуальной функции тестирования. Для удаления базы данных после окончания теста, мы закрываем файл и удаляем его из файловой системы в методе :meth:`~unittest.TestCase.tearDown`. Дополнительно, в процессе настройки активируется флаг конфигурации ``TESTING``. Это приводит к отключению отлова ошибок во время обработки запроса, и вы получаете более качественные отчёты об ошибках при выполнении по отношению к приложению тестовых запросов. Этот тестовый клиент даст нам простой интерфейс к приложению. Мы можем запускать тестовые запросы к приложению, а клиент будет также отслеживать для нас cookies. Так как SQLite3 использует файловую систему, для создания временной базы данных и её инициализации мы можем по-простому использовать модуль tempfile. Функция :func:`~tempfile.mkstemp` делает для нас две вещи: она возвращает низкоуровневый обработчик файла и случайное имя файла, последний из которых мы будем использовать как имя базы данных. Нам всего лишь надо сохранить значение `db_fd`, чтобы в дальнейшем мы могли использовать функцию :func:`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`; это позволяет модулю :mod:`unittest` определить метод как тест для запуска. С помощью использования `self.app.get` мы можем послать приложению HTTP-запрос `GET` с заданным путём. Возвращаемым значением будет объект :class:`~flask.Flask.response_class`. Теперь мы можем для проверки возвращаемого значения (как строки) использовать атрибут :attr:`~werkzeug.wrappers.BaseResponse.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='', text='HTML allowed here' ), follow_redirects=True) assert 'No entries here so far' not in rv.data assert '<Hello>' in rv.data assert 'HTML allowed here' in rv.data Здесь мы проверяем, что запись соответсвует установленным правилам - HTML может присутствовать в тексте, но не заголовке. Теперь при запуске мы должны увидеть, что пройдено 3 теста:: $ python flaskr_tests.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.332s OK Чтобы посмотреть более сложные тесты заголовков и статусных кодов, обратитесь к исходникам Flask `MiniTwit Example`_, в которых есть более развёрнутый набор тестов. .. _MiniTwit Example: http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ Другие трюки с тестами ---------------------- Кроме вышеуказанного использования клиента тестирования, есть ещё метод :meth:`~flask.Flask.test_request_context`, который может быть использован в комбинации с оператором `with` для временной активации контекста запроса. С помощью него вы можете получить доступ к объектам :class:`~flask.request`, :class:`~flask.g` и :class:`~flask.session`, подобно функции представления. Вот полный пример, который демонстрирует подобный подход:: app = flask.Flask(__name__) with app.test_request_context('/?name=Peter'): assert flask.request.path == '/' assert flask.request.args['name'] == 'Peter' Подобным способом могут использоваться и все другие объекты, связанные с контекстом. Если вы хотите протестировать ваше приложение в другой конфигурации, а хорошего способа, чтобы это сделать, нет, подумайте над переходом на фабрики приложений (см. :ref:`app-factories`). Заметим, однако, что если вы используете контекст тестового запроса, функции :meth:`~flask.Flask.before_request` автоматически не выполняются для подобных функций :meth:`~flask.Flask.after_request`. Однако, функции :meth:`~flask.Flask.teardown_request` действительно выполняются, когда контекст тестового запроса покидает блок `with`. Если вы хотите, чтобы функции :meth:`~flask.Flask.before_request` также вызывались, вам необходимо вручную использовать вызов :meth:`~flask.Flask.preprocess_request`:: app = flask.Flask(__name__) with app.test_request_context('/?name=Peter'): app.preprocess_request() ... Это может понадобиться для открытия соединений с базой данных или для чего-то подобного, в зависимости от того, как построено ваше приложение. Если вы хотите вызвать функции :meth:`~flask.Flask.after_request`, вам необходимо воспользоваться вызовом :meth:`~flask.Flask.process_response`, однако он требует, чтобы вы передали ему объект ответа:: app = flask.Flask(__name__) with app.test_request_context('/?name=Peter'): resp = Response('...') resp = app.process_response(resp) ... Обычно, это не так полезно, так как в данный момент вы можете прямо стартовать с помощью тестового клиента. .. _faking-resources: Подделка ресурсов и контекста ----------------------------- .. versionadded:: 0.10 Часто используемая ситуация, когда информация о пользовательской авторизации и соединениях с базой данных сохраняется в контексте приложения или в объекте :attr:`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) сигнал :data:`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) Сохранение окружения контекста ------------------------------ .. versionadded:: 0.4 Иногда бывает полезно вызвать регулярный запрос, сохранив при этом немного дольше окружение контекста, так чтобы могла произойти дополнительная интроспекция. Это стало возможным начиная с Flask версии 0.4 с использованием :meth:`~flask.Flask.test_client` совместно с блоком `with`:: app = flask.Flask(__name__) with app.test_client() as c: rv = c.get('/?tequila=42') assert request.args['tequila'] == '42' Если вы использовали просто :meth:`~flask.Flask.test_client` без блока `with`, `assert` завершится аварийно с ошибкой, так как `request` более недоступен (потому что вы попытались использовать его вне актуального запроса). Доступ к сессиям и их изменение ------------------------------- .. versionadded:: 0.8 Иногда бывает очень полезно получить доступ к сессиям или изменить их из клиента тестирования. Обычно, чтобы сделать это, существует два способа. Если вы просто хотите убедиться, что в сессии есть определённый набор ключей, которым присвоены определённые значения, вам надо просто сохранить окружение контекста и получить доступ к :data:`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' # здесь сессия будет сохранена Заметим, что в данном случае вам необходимо использовать вместо прокси :data:`flask.session` объект ``sess``. Однако, объект сам по себе обеспечит вас тем же интерфейсом. `Оригинал этой страницы `_