Тестирование приложений 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 '&lt;Hello&gt;' 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. Однако, объект сам по себе обеспечит вас тем же интерфейсом.

Оригинал этой страницы