Modern QA2026Валидация ответов
Join

Course14 API Testing Fundamentals

Foundations · Chapter 14

Валидация ответов

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 со следующими эндпоинтами:

  1. GET /products — возвращает пагинированный список с items, total, page, per_page
  2. GET /products/:id — возвращает один продукт с id, name, price, category
  3. POST /products — создаёт продукт, возвращает 201 с новым продуктом
  4. Протестируйте пагинацию: отсутствие перекрытия, корректные итоги, граничные страницы
  5. Протестируйте структуру ответа: обязательные поля, правильные типы, отсутствие конфиденциальных данных
  6. Протестируйте сортировку: проверьте, что sort=price возвращает элементы в порядке возрастания цены

Ключевые выводы

  • Валидируйте больше, чем коды состояния: проверяйте структуру ответа, типы и отсутствие конфиденциальных данных
  • Тестирование пагинации обнаруживает перекрытия, пропуски и некорректные итоги
  • Используйте JSON Schema для строгой структурной валидации
  • Тестируйте сортировку и фильтрацию для проверки корректности параметров запроса
  • Утверждения о времени ответа обнаруживают регрессии производительности на ранних этапах