Spec-Zone.ru › Flask 1.0

Тестирование приложений 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'&lt;Hello&gt;' 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/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API