Тестирование 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-ответ
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()
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
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
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
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
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 ====================Другие расширения