Как выполнить юнит-тестирование в Flask

Введение

Testing является неотъемлемой частью процесса разработки программного обеспечения, он обеспечивает то, что код ведет себя как ожидается и free of defects. В Python pytest является популярным фреймворком для тестирования, предлагающим несколько преимуществ по сравнению с стандартным модулем unit test, который является встроенным модулем тестирования Python и входит в стандартную библиотеку. pytest включает более простой синтаксис, лучший вывод, мощные данные задачи и богатую экосистему плагинов. Это руководство вам поможет пройти процесс настройки приложения Flask, интегрировать данные задачи pytest и написать единичные тесты с использованием pytest.

Предупреждения

Перед началом вам потребуется следующее:

  • Сервер с установленной системой Ubuntu и пользователь не-root, имеющий права sudo и активную firewall. Чтобы получить инструкции по установке, пожалуйста выберите из этого списка вашу дистрибуцию и следуйте指南 настройки начального сервера. Пожалуйста, убедитесь, что работаете с поддерживаемой версией Ubuntu.

  • Освоенность с использованием командной строки Linux. Вы можете ознакомиться с информацией в этой guide on Linux command line primer.

  • Основное понимание Python программирования и тестирования с использованием фреймворка pytest в Python. Чтобы перейти к дополнительным сведениям о pytest, вы можете обратиться к нашему учебнику по PyTest Python Testing Framework.

  • Python 3.7 или более новая версия установлена на вашей системе Ubuntu. Чтобы узнать, как запускать сценарий Python на Ubuntu, вы можете прочитать нашу статью Как запустить сценарий Python на Ubuntu.

Почему pytest является лучшей альтернативой unittest

pytest предлагает несколько преимуществ над встроенным фреймворком unittest:

  • Pytest позволяет вам писать тесты с меньшим количеством шаблонного кода, используя простые операторы assert вместо более развёрнутых методов, требуемых unittest.

  • Он обеспечивает более детальные и читабельные выводы, что помогает легче определить, где и почему тест не сработал.

  • Фикшены Pytest позволяют работать с более гибкими и реusable тестовыми установками, чем методы setUp и tearDown unittest.
  • Он упрощает запуск того же тестового функции с несколькими наборами входных данных, что не так просто в unittest.

  • Pytest имеет обширное собрание плагинов, которые extends его функциональность, от инструментов охвата кода до параллельного выполнения тестов.

  • Он автоматически обнаруживает тестовые файлы и функции, соответствующие его наименованиям, экономя вам время и усилия в управлении наборами тестов.

Давая такие преимущества, pytest часто является предпочтительным выбором для современного тестирования на Python. Давайте настроим приложение Flask и напишем модульные тесты, используя pytest.

Шаг 1 – Настройка среды

Ubuntu 24.04 по умолчанию идет с Python 3. Откройте терминал и выполните следующую команду, чтобы удостовериться в установке Python 3:

root@ubuntu:~# python3 --version
Python 3.12.3

Если Python 3 уже установлен на вашей машине, вышеуказанная команда вернет текущую версию установленного Python 3. Если он не установлен, вы можете выполнить следующую команду и установить Python 3:

root@ubuntu:~# sudo apt install python3

Далее вам нужно установить установщик пакетов pip в вашей системе:

root@ubuntu:~# sudo apt install python3-pip

Как только pip установлен, давайте установим Flask.

Шаг 2 – Создание приложения Flask

Начнем с создания простого приложения Flask. Создайте новую директорию для вашего проекта и переместитесь в нее:

root@ubuntu:~# mkdir flask_testing_app
root@ubuntu:~# cd flask_testing_app

Теперь создадим и активируем виртуальную среду для управления зависимостями:

root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# source venv/bin/activate

Установим Flask с использованием pip:

root@ubuntu:~# pip install Flask

Теперь создадим простую Flask-апликацию. Создайте новый файл с именем app.py и добавьте следующий код:

app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello, Flask!")

@app.route('/about')
def about():
    return jsonify(message="This is the About page")

@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
    result = x * y
    return jsonify(result=result)

if __name__ == '__main__':
    app.run(debug=True)

Эта апликация имеет три маршрута:

  • /: возвращает простое сообщение “Привет, Flask!”.
  • /about: возвращает простое сообщение “Это страница о проекте”.
  • /multiply/<int:x>/<int:y>: умножает два целых числа и возвращает результат.

Чтобы запустить приложение, выполните следующий приказ:

root@ubuntu:~# flask run
output
* Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Согласно вышенапечатанному выводу вы можете заметить, что сервер запущен на http://127.0.0.1 и принимает соединения на порту 5000. Откройте другой консоль Ubuntu и выполните нижеследующий curl запрос один за другим:

  • GET: curl http://127.0.0.1:5000/
  • GET: curl http://127.0.0.1:5000/about
  • GET: curl http://127.0.0.1:5000/multiply/10/20

Давайте посмотрим, что делают эти GET-запросы:

  1. curl http://127.0.0.1:5000/:
    Это отправляет GET-запрос на корневую URL (‘/’) нашего Flask-приложения. Сервер отвечает JSON-объектом с сообщением “Hello, Flask!”, демонстрируя базовую функциональность нашего домашнего маршрута.

  2. curl http://127.0.0.1:5000/about:
    Это отправляет GET-запрос на маршрут /about. Сервер отвечает JSON-объектом с сообщением “This is the About page”. Это показывает, что наш маршрут работает корректно.

  3. curl http://127.0.0.1:5000/multiply/10/20:
    Это отправляет GET-запрос на маршрут /multiply с двумя параметрами: 10 и 20. Сервер умножает эти числа и отвечает JSON-объектом с результатом (200). Это демонстрирует, что наш маршрут для умножения правильно обрабатывает URL-параметры и выполняет вычисления.

Эти GET-запросы позволяют нам взаимодействовать с API-конечными точками нашего Flask-приложения, получая информацию или вызывая действия на сервере без изменения данных. Их используют для извлечения данных, тестирования функциionalности концептных точек и проверки, что наши маршруты отвечают ожидаемо.

Посмотрим на действие каждого из этих GET-запросов:

root@ubuntu:~# curl http://127.0.0.1:5000/
Output
{"message":"Hello, Flask!"}
root@ubuntu:~# curl http://127.0.0.1:5000/about
Output
{"message":"This is the About page"}
root@ubuntu:~# curl http://127.0.0.1:5000/multiply/10/20
Output
{"result":200}

Шаг 3 – Установка pytest и написание первого теста

Теперь, когда у вас есть базовое Flask-приложение, установим pytest и напишем несколько модульных тестов.

Установите pytest с помощью pip:

root@ubuntu:~# pip install pytest

Создайте каталог tests для хранения ваших тестовых файлов:

root@ubuntu:~# mkdir tests

Теперь создадим новый файл с именем test_app.py и добавим следующий код:

test_app.py
# Импортируем модуль sys для измененияRuntime Environment Python
import sys
# Импортируем модуль os для взаимодействия с операционной системой
import os

# Добавляем родительскую директорию в sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Импортируем экземпляр Flask app из основного app файла
from app import app 
# Импортируем pytest для написания и выполнения тестов
import pytest

@pytest.fixture
def client():
    """A test client for the app."""
    with app.test_client() as client:
        yield client

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Давайте посмотрим на функции в этом тестовом файле:

  1. @pytest.fixture def client():
    Это fixtures pytest, который создает тестовый клиент для нашей Flask app. Он использует app.test_client() метод для создания клиента, который может отправлять запросы в нашу app без запуска реального сервера. СтаEMENT yield позволяет использовать клиент в тестах и затем правильно закрывать его после каждого теста.

  2. def test_home(client):

    Эта функция тестирует домашнюю роут (/) нашего приложения. Она отправляет GET-запрос по этому роуту с помощью тестового клиента, затем утверждает, что статус-код ответа равен 200 (OK) и что JSON-ответ соответствует ожидаемому сообщению.

  3. def test_about(client):

    Подобно test_home, эта функция тестирует роут о себе (/about). Она проверяет наличие статус-кода 200 и проверяет содержимое JSON-ответа.

  4. def test_multiply(client):

    Эта функция тестирует роут умножения с правильными входными данными (/multiply/3/4). Она проверяет, что статус-код равен 200, и что JSON-ответ содержит правильный результат умножения.

  5. def test_multiply_invalid_input(client):
    Эта функция тестирует маршрут для умножения с неверными данными ввода (multiply/three/four). Она проверяет, что статус-код ответа равен 404 (Не найдено), что соответствует ожидаемому поведению, когда маршрут не может соответствовать строковому вводу требуемым целочисленными параметрами.

  6. def test_non_existent_route(client):
    Эта функция тестирует поведение приложения, когда доступен несуществующий маршрут. Она отправляет GET-запрос на /non-existent, который не определен в нашем Flask-приложении. Тест утверждает, что статус-код ответа равен 404 (Не найдено), что позволяет убедиться, что наш сервис правильно обрабатывает запросы на не определенные маршруты.

Эти тесты охватывают базовую функциональность нашего Flask-приложения, убеждаясь, что каждый маршрут правильно реагирует на допустимые входные данные и что маршрут умножения соответствующе обрабатывает неверные входные данные. Utilizing pytest, we can easily run these tests to verify that our app is working as expected.

Шаг 4 – Запуск тестов

Для запуска тестов выполните следующую команду:

root@ubuntu:~# pytest

по умолчанию, процесс обнаружения pytest будет рекурсивно искать в текущем каталоге и его подкаталогах файлы, начинающиеся с именем “test_” или заканчивающиеся на “_test”. Тесты, расположенные в этих файлах, затем будут выполнены. Вы должны увидеть вывод, похожий на:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app collected 5 items tests/test_app.py .... [100%] ======================================================= 5 passed in 0.19s ========================================================

Это указывает, что все тесты успешно прошли.

Шаг 5: Использование fixture в pytest

fixture – это функции, используемые для предоставления данных или ресурсов тестам. Они могут использоваться для установки и демонтажа тестовых средств, загрузки данных или выполнения других операций настройки. В pytest fixture определяются с помощью декоратора @pytest.fixture.

Вот как можно улучшить существующий fixture. Обновите fixture клиента, используя логику установки и разрушения:

test_app.py
@pytest.fixture
def client():
    """Set up a test client for the app with setup and teardown logic."""
    print("\nSetting up the test client")
    with app.test_client() as client:
        yield client  # Это место, где происходит тестирование
    print("Tearing down the test client")

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Данное установка добавляет печатные заявления, чтобы продемонстрировать фазы установки и разрушения в выходе тестов. Эти могут быть заменены на реальный код управления ресурсами, если это необходимо.

Попробуем запустить тесты снова:

root@ubuntu:~# pytest -vs

Flag -v увеличивает verbosity, а flag -s позволяет печатным заявлениям отображаться в консольном выводе.

Вы должны увидеть следующий вывод:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 5 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client ============================================ 5 passed in 0.35s =============================================

Шаг 6: Добавление теста с ошибкой

Добавим тест с ошибкой в существующий файл теста. Измените файл test_app.py и добавьте ниже функцию в конце для теста с ошибкой для неверного результата:

test_app.py
def test_multiply_edge_cases(client):
    """Test the multiply route with edge cases to demonstrate failing tests."""
    # Test with zero
    response = client.get('/multiply/0/5')
    assert response.status_code == 200
    assert response.json == {"result": 0}

    # Test with large numbers (this might fail if not handled properly)
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

    # Intentional failing test: incorrect result
    response = client.get('/multiply/2/3')
    assert response.status_code == 200
    assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case"

Разбираемся с функцией test_multiply_edge_cases и объясняем, что делает каждая ее часть:

  1. Test with zero:
    Это тест проверяет, правильно ли функция умножения обрабатывает умножение на нуль. Мы ожидаем, что результат будет 0, когда любое число умножается на нуль. Это важное крайнее значение для тестирования,因為 некоторые реализации могут иметь проблемы с умножением на нуль.

  2. Test с большими числами:
    Этот тест проверяет, whether функция умножения может обрабатывать большие числа без переполнения или проблем с точностью. Мы умножаем два значения в миллион, ожидая результата в один триллион. Этот тест важен, because он проверяет верхние ограничения возможностей функции. Note, что это может потерпеть неудачу, if реализация сервера не умеет обрабатывать большие числа правильно, что может указать на необходимость библиотек с большими числами или другого типа данных.

  3. Test на намеренное сбой:
    Этот тест нарочно установлен, чтобы провалиться. Он проверяет, whether 2 * 3 равно 7, что является неверным. Этот тест направлен на демонстрацию внешнего вида теста при сбое. Это помогает понять, как выглядят проваленные тесты в выводе тестов, which is an essential skill in test-driven development and debugging processes.

При включении这些 крайних случаев и нарочитого провала вы проверяете не только базовую функциональность вашего многочлена маршрута, но также и его поведение в экстремальных условиях и его способности по сообщению ошибок. Этот подход к тестированию помогает обеспечить надежность и устойчивость нашей приложения.

Попробуем запустить тесты снова:

root@ubuntu:~# pytest -vs

Вы должны увидеть следующий вывод:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 6 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_edge_cases Setting up the test client FAILED Tearing down the test client ================================================================= FAILURES ================================================================== _________________________________________________________ test_multiply_edge_cases __________________________________________________________ client = <FlaskClient <Flask 'app'>> def test_multiply_edge_cases(client): """Test the multiply route with edge cases to demonstrate failing tests.""" # Тест с нулевым значением response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # Тест с большими числами (это может провалиться, если не учитывается правильно) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # Нарочитый провальный тест: неверный результат response = client.get('/multiply/2/3') assert response.status_code == 200 > assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case" E AssertionError: This test should fail to demonstrate a failing case E assert {'result': 6} == {'result': 7} E E Differing items: E {'result': 6} != {'result': 7} E E Full diff: E { E - 'result': 7,... E E ...Full output truncated (4 lines hidden), use '-vv' to show tests/test_app.py:61: AssertionError ========================================================== short test summary info ========================================================== FAILED tests/test_app.py::test_multiply_edge_cases - AssertionError: This test should fail to demonstrate a failing case ======================================================== 1 failed, 5 passed in 0.32s ========================================================

Сообщение ошибки, указанное выше, указывает, что тест test_multiply_edge_cases в файле tests/test_app.py провалился. SPECIFICALLY, last assert в этой тестовой функции вызвало провал.

Этот нарочитый провал полезен для демонстрации, как сообщаются ошибки в тестах и какая информация предоставляется в сообщениях о провалах. Оно показывает точную строку, где произошел провал, ожидаемые и фактические значения и разница между ними.

В реальных условиях вы бы исправили код, чтобы тест прошел успешно, или настроили тест, если ожидаемый результат был неверным. Однако в этом случае ошибка произвольно установлена для учебных целей.

Заключение

В этом учебнике мы рассмотрели, как настроить тесты модуля для приложения Flask с использованием pytest, внедрить fixture pytest и продемонстрировали, как выглядят провалы тестов. Следуя этим шагам, вы можете убедиться, что ваши приложения Flask надежны и поддерживаемые, минимизировав bugs и улучшив качество кода.

Вы можете обратиться к официальной документации Flask и официальной документации Pytest, чтобы перейти на более глубокое знание.

Source:
https://www.digitalocean.com/community/tutorials/unit-test-in-flask