Тестирование Flask

Тестирование — основа обеспечения качества кода.

Flask предоставляет встроенный тестовый клиент, который в сочетании с pytest позволяет эффективно писать и запускать тесты.


Тестовый клиент——test_client

Flasktest_client()Метод создаёт имитированный HTTP-клиент, который может отправлять запросы без запуска сервера:

Пример

# Путь к файлу: test_demo.py (тест в интерактивной среде Python)
from app import create_app

# Создание экземпляра тестового приложения
app = create_app()

# Создайте тестовый клиент
with app.test_client() as client:
    # Отправить GET-запрос
    response = client.get("/")
    print(response.status_code)   # 200
    print(response.data[:100])    # Первые 100 байт тела ответа

    # Отправить POST-запрос и передать данные как JSON
    response = client.post("/api/posts",
                          json={"title": "EXAMPLE Tutorial", "body": "content", "author_id": 1})
    print(response.status_code)   # 201
    print(response.get_json())    # Разобрать JSON-ответ

Интеграция с pytest

pytest — стандартный фреймворк для тестирования в сообществе Python, и он очень удобно работает с Flask.

Установка pytest

(.venv) $ pip install pytest

Написание тестов——conftest.py (общие фикстуры)

Использованиеconftest.pyФайл создаёт переиспользуемые тестовые фикстуры:

Пример

# Путь к файлу: tests/conftest.py
import os
import tempfile
import pytest
from app import create_app
from db import init_db, get_db

@pytest.fixture
def app():
    """Создать тестовое приложение с использованием временной базы данных"""
    # Создать временный файл как тестовую базу данных
    db_fd, db_path = tempfile.mkstemp()

    # Создать, переопределить путь к базе данных на временный файл
    app = create_app()
    app.config["DATABASE"] = db_path
    app.config["TESTING"] = True  # Включить тестовый режим

    # Инициализировать структуры таблиц тестовой базы данных
    with app.app_context():
        init_db()

    yield app

    # Очистка после тестов: закрыть и удалить временный файл базы данных
    os.close(db_fd)
    os.unlink(db_path)

@pytest.fixture
def client(app):
    """Создать тестовый клиент"""
    return app.test_client()

@pytest.fixture
def runner(app):
    """Создать CLI-раннер для тестов"""
    return app.test_cli_runner()

Тестирование маршрутов

Пример

# Путь к файлу: tests/test_app.py
def test_home_page(client):
    """Проверяет, что главная страница возвращается нормально"""
    response = client.get("/")
    assert response.status_code == 200
    # Проверить, содержит ли страница ожидаемые ключевые слова
    assert b"EXAMPLE" in response.data

def test_404_page(client):
    """Проверка посещения несуществующей страницы"""
    response = client.get("/this-page-does-not-exist")
    assert response.status_code == 404

Запуск тестов:

(.venv) $ pytest tests/test_app.py -v
====================================
tests/test_app.py::test_home_page PASSED
tests/test_app.py::test_404_page PASSED
====================================

Тестирование JSON API

Пример

# Путь к файлу: tests/test_api.py
def test_create_post(client):
    """Тест API создания статьи"""
    response = client.post("/api/posts", json={
        "title": "ПРИМЕР тестовой статьи",
        "body": "Это тестовое содержимое",
        "author_id": 1
    })
    # Успешное создание должно возвращать 201
    assert response.status_code == 201
    data = response.get_json()
    assert data["title"] == "ПРИМЕР тестовой статьи"
    assert "id" in data

def test_create_post_without_title(client):
    """Проверка, что пустой заголовок возвращает ошибку 400"""
    response = client.post("/api/posts", json={
        "title": "",
        "body": "содержимое",
        "author_id": 1
    })
    assert response.status_code == 400
    assert b"error" in response.data

def test_get_posts(client):
    """Тест получения списка статей"""
    # Сначала создаём статью
    client.post("/api/posts", json={
        "title": "Статья A", "body": "Содержимое A", "author_id": 1
    })

    # Получаем список
    response = client.get("/api/posts")
    assert response.status_code == 200
    data = response.get_json()
    assert len(data) >= 1

def test_delete_post(client):
    """Тест удаления статьи"""
    # Сначала создаём
    rv = client.post("/api/posts", json={
        "title": "Для удаления", "body": "content", "author_id": 1
    })
    post_id = rv.get_json()["id"]

    # Затем удалить
    rv = client.delete(f"/api/posts/{post_id}")
    assert rv.status_code == 200

    # Убедиться, что удалено
    rv = client.get(f"/api/posts/{post_id}")
    assert rv.status_code == 404

Тестирование сессии

Тестирование возможностей, требующих сессии, таких как вход:

Пример

# Путь к файлу: tests/test_auth.py
def test_login(client):
    """Тестирование процесса входа"""
    # Отправить запрос на вход
    response = client.post("/auth/login", data={
        "username": "testuser",
        "password": "testpass"
    }, follow_redirects=True)  # follow_redirects автоматически следует за перенаправлениями

    assert response.status_code == 200

def test_session_transaction(client):
    """Напрямую манипулировать сессией для тестирования"""
    with client.session_transaction() as session:
        # Напрямую задать значения в сессии, чтобы имитировать состояние входа
        session["username"] = "example"

    # Теперь перейдите на главную страницу; она должна отображать статус входа
    response = client.get("/")
    assert b"example" in response.data

client.session_transaction()Это очень практичный инструмент для тестирования — он позволяет напрямую изменять содержимое сессии без прохождения полного процесса входа.


Тестирование команд CLI

Пример

# Путь к файлу: tests/test_cli.py
def test_init_db(runner):
    """Проверка команды инициализации базы данных"""
    result = runner.invoke(args=["init-db"])
    # Команда успешно выполнена; exit_code должен быть 0
    assert result.exit_code == 0

def test_flask_routes(runner):
    """Проверка команды flask routes"""
    result = runner.invoke(args=["routes"])
    assert result.exit_code == 0
    # Проверить, содержит ли вывод зарегистрированные маршруты
    assert "/api/posts" in result.output

Лучшие практики тестирования

Практика Описание
Использование временной базы данных Каждый тест использует независимую базу данных, чтобы они не мешали друг другу (например, tempfile в conftest.py)
Включение режима TESTING app.config["TESTING"] = TrueПозволить исключениям распространяться как обычно, а не подавляться обработкой ошибок
Один тест должен проверять только одну вещь. Каждая тестовая функция проверяет только одно поведение, что упрощает поиск проблем
Понятное именование Имена функций выглядят так:test_<scenario>_<expected_result>
Активно используйте follow_redirects Полезно при тестировании потока перенаправлений после POST

Запустить все тесты

(.venv) $ pytest tests/ -v

tests/test_app.py::test_home_page PASSED
tests/test_app.py::test_404_page PASSED
tests/test_api.py::test_create_post PASSED
tests/test_api.py::test_create_post_without_title PASSED
tests/test_api.py::test_get_posts PASSED
tests/test_api.py::test_delete_post PASSED
tests/test_auth.py::test_login PASSED
tests/test_auth.py::test_session_transaction PASSED
tests/test_cli.py::test_init_db PASSED
tests/test_cli.py::test_flask_routes PASSED

==================== 10 passed in 0.45s ====================
Другие расширения