Валидация ответов
Updated Jul 2026
Валидация ответов API — это больше, чем проверка кодов состояния. Комплексная стратегия валидации проверяет коды состояния, структуру ответа, типы данных, наличие полей, корректность пагинации и отсутствие конфиденциальных данных.
Коды состояния для проверки
Тестируйте как ожидаемый код успеха, так и режимы отказа:
| Сценарий | Ожидаемый код | Что проверять |
|---|---|---|
| Успешный GET | 200 | Тело содержит ожидаемые данные |
| Успешный POST | 201 | Тело или заголовок Location содержит ID нового ресурса |
| Успешный DELETE | 204 | Последующий GET возвращает 404 |
| Ошибка валидации | 400 или 422 | Сообщение об ошибке указывает невалидное поле |
| Не авторизован | 401 | Отсутствующий или просроченный токен |
| Запрещено | 403 | Валидный токен, недостаточные разрешения |
| Не найдено | 404 | Несуществующий ресурс |
| Конфликт | 409 | Попытка создания дубликата |
def test_get_user_success(api, create_user):
user = create_user(name="Alice")
r = api.get(f"/users/{user['id']}")
assert r.status_code == 200
def test_get_nonexistent_user(api):
r = api.get("/users/99999999")
assert r.status_code == 404
def test_create_duplicate_email(api, create_user):
user = create_user(email="unique@test.com")
r = api.post("/users", json={"name": "Duplicate", "email": "unique@test.com"})
assert r.status_code == 409
Валидация структуры ответа
Проверьте, что тело ответа содержит все ожидаемые поля с правильными типами и не содержит конфиденциальных данных.
def test_list_users_structure(api):
r = api.get("/users")
data = r.json()
# Top-level pagination structure
assert {"items", "total", "page", "per_page"}.issubset(data.keys())
assert isinstance(data["total"], int)
assert isinstance(data["items"], list)
# Individual item structure
if data["items"]:
user = data["items"][0]
assert {"id", "name", "email", "created_at"}.issubset(user.keys())
assert isinstance(user["id"], int)
assert isinstance(user["name"], str)
assert "@" in user["email"]
# No sensitive data
assert "password_hash" not in user
assert "ssn" not in user
assert "credit_card" not in user
def test_user_detail_structure(api, create_user):
user = create_user()
r = api.get(f"/users/{user['id']}")
data = r.json()
# Detailed response may have more fields
required = {"id", "name", "email", "role", "created_at", "updated_at"}
assert required.issubset(data.keys()), f"Missing: {required - data.keys()}"
# Type checking
assert isinstance(data["id"], int)
assert isinstance(data["role"], str)
assert data["role"] in {"admin", "editor", "viewer"}
Валидация схемы с jsonschema
Для более строгой валидации используйте JSON Schema:
from jsonschema import validate
USER_SCHEMA = {
"type": "object",
"required": ["id", "name", "email", "role", "created_at"],
"properties": {
"id": {"type": "integer"},
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
"role": {"type": "string", "enum": ["admin", "editor", "viewer"]},
"created_at": {"type": "string", "format": "date-time"},
},
"additionalProperties": False # No unexpected fields
}
def test_user_matches_schema(api, create_user):
user = create_user()
r = api.get(f"/users/{user['id']}")
validate(instance=r.json(), schema=USER_SCHEMA)
Тестирование пагинации
Пагинация — частый источник багов: перекрытие страниц, пропущенные элементы, некорректные итоги.
def test_pagination_no_overlap(api):
"""Pages should not contain overlapping items."""
r1 = api.get("/users?page=1&per_page=10")
r2 = api.get("/users?page=2&per_page=10")
ids1 = {u["id"] for u in r1.json()["items"]}
ids2 = {u["id"] for u in r2.json()["items"]}
assert ids1.isdisjoint(ids2), f"Overlapping IDs: {ids1 & ids2}"
def test_pagination_total_consistency(api):
"""Total count should be consistent across pages."""
r1 = api.get("/users?page=1&per_page=10")
r2 = api.get("/users?page=2&per_page=10")
assert r1.json()["total"] == r2.json()["total"]
def test_pagination_all_items_covered(api):
"""Collecting all pages should yield exactly 'total' items."""
all_ids = set()
page = 1
total = None
while True:
r = api.get(f"/users?page={page}&per_page=50")
data = r.json()
total = data["total"]
items = data["items"]
if not items:
break
all_ids.update(u["id"] for u in items)
page += 1
assert len(all_ids) == total
def test_pagination_boundary(api):
"""Request a page beyond the last page."""
r = api.get("/users?page=99999&per_page=10")
assert r.status_code == 200
assert r.json()["items"] == []
def test_pagination_invalid_params(api):
"""Invalid pagination parameters should return errors."""
r = api.get("/users?page=0&per_page=10")
assert r.status_code == 400
r = api.get("/users?page=1&per_page=0")
assert r.status_code == 400
r = api.get("/users?page=-1&per_page=10")
assert r.status_code == 400
Валидация сортировки и фильтрации
def test_sort_by_created_at_descending(api):
r = api.get("/users?sort=-created_at")
items = r.json()["items"]
dates = [item["created_at"] for item in items]
assert dates == sorted(dates, reverse=True), "Items not sorted by created_at desc"
def test_filter_by_role(api, create_user):
create_user(role="admin")
create_user(role="viewer")
r = api.get("/users?role=admin")
items = r.json()["items"]
assert all(u["role"] == "admin" for u in items), "Filter returned non-admin users"
def test_search_query(api, create_user):
create_user(name="UniqueSearchName123")
r = api.get("/users?q=UniqueSearchName123")
items = r.json()["items"]
assert len(items) >= 1
assert any(u["name"] == "UniqueSearchName123" for u in items)
Валидация времени ответа
def test_list_users_response_time(api):
r = api.get("/users")
assert r.elapsed.total_seconds() < 2.0, \
f"Response took {r.elapsed.total_seconds():.2f}s (limit: 2s)"
def test_health_check_fast(base_url):
r = requests.get(f"{base_url}/health")
assert r.elapsed.total_seconds() < 0.5
Практическое упражнение
Напишите тесты валидации ответов для API со следующими эндпоинтами:
GET /products— возвращает пагинированный список с items, total, page, per_pageGET /products/:id— возвращает один продукт с id, name, price, categoryPOST /products— создаёт продукт, возвращает 201 с новым продуктом- Протестируйте пагинацию: отсутствие перекрытия, корректные итоги, граничные страницы
- Протестируйте структуру ответа: обязательные поля, правильные типы, отсутствие конфиденциальных данных
- Протестируйте сортировку: проверьте, что
sort=priceвозвращает элементы в порядке возрастания цены
Ключевые выводы
- Валидируйте больше, чем коды состояния: проверяйте структуру ответа, типы и отсутствие конфиденциальных данных
- Тестирование пагинации обнаруживает перекрытия, пропуски и некорректные итоги
- Используйте JSON Schema для строгой структурной валидации
- Тестируйте сортировку и фильтрацию для проверки корректности параметров запроса
- Утверждения о времени ответа обнаруживают регрессии производительности на ранних этапах