Modern QA2026Примеры тестов API, сгенерированных ИИ
Join

Course04 API & Contract Testing with AI

Cutting-edge · Chapter 04

Примеры тестов API, сгенерированных ИИ

Updated Jul 2026

Полный пример сгенерированных тестов

Этот файл демонстрирует, что создаёт ИИ на основе OpenAPI-схемы при правильно составленном промпте, с комментариями о том, что делает каждый тест эффективным.

Сгенерированный файл тестов

import pytest
import httpx
from uuid import uuid4

BASE_URL = "https://api.staging.example.com"


# ============================================================
# Fixtures
# ============================================================

@pytest.fixture
def auth_token():
    """Get a valid user-level auth token."""
    return get_test_token(role="user")

@pytest.fixture
def admin_token():
    """Get a valid admin-level auth token."""
    return get_test_token(role="admin")

@pytest.fixture
def seed_product(admin_token):
    """Create a product for testing, clean up afterward."""
    response = httpx.post(
        f"{BASE_URL}/api/v2/products",
        headers={"Authorization": f"Bearer {admin_token}"},
        json={"name": "Test Widget", "price": 29.99, "category": "electronics"}
    )
    product = response.json()
    yield product
    # Cleanup
    httpx.delete(
        f"{BASE_URL}/api/v2/products/{product['id']}",
        headers={"Authorization": f"Bearer {admin_token}"}
    )


# ============================================================
# GET /api/v2/products/{id}
# ============================================================

class TestGetProduct:
    """GET /api/v2/products/{id}"""

    def test_get_existing_product(self, auth_token, seed_product):
        """Happy path: retrieve a known product."""
        response = httpx.get(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {auth_token}"}
        )
        assert response.status_code == 200
        body = response.json()
        assert body["id"] == seed_product["id"]
        assert body["name"] == seed_product["name"]
        assert body["category"] in ["electronics", "clothing", "food", "other"]
        assert isinstance(body["price"], (int, float))
        assert body["price"] >= 0

    def test_get_nonexistent_product(self, auth_token):
        """404 when product ID does not exist."""
        fake_id = str(uuid4())
        response = httpx.get(
            f"{BASE_URL}/api/v2/products/{fake_id}",
            headers={"Authorization": f"Bearer {auth_token}"}
        )
        assert response.status_code == 404

    def test_get_product_invalid_uuid(self, auth_token):
        """400 or 422 when ID is not a valid UUID."""
        response = httpx.get(
            f"{BASE_URL}/api/v2/products/not-a-uuid",
            headers={"Authorization": f"Bearer {auth_token}"}
        )
        assert response.status_code in [400, 422]

    def test_get_product_no_auth(self):
        """401 when no authorization header is provided."""
        response = httpx.get(f"{BASE_URL}/api/v2/products/{uuid4()}")
        assert response.status_code == 401


# ============================================================
# PUT /api/v2/products/{id}
# ============================================================

class TestUpdateProduct:
    """PUT /api/v2/products/{id}"""

    def test_update_product_as_admin(self, admin_token, seed_product):
        """Happy path: admin updates a product."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"name": "Updated Widget", "price": 29.99, "category": "electronics"}
        )
        assert response.status_code == 200
        assert response.json()["name"] == "Updated Widget"

    def test_update_product_as_non_admin(self, auth_token, seed_product):
        """403 when non-admin tries to update."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {auth_token}"},
            json={"name": "Hacked", "price": 0}
        )
        assert response.status_code == 403

    def test_update_missing_required_name(self, admin_token, seed_product):
        """400 when required field 'name' is missing."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"price": 10.00}  # name is missing
        )
        assert response.status_code == 400

    def test_update_missing_required_price(self, admin_token, seed_product):
        """400 when required field 'price' is missing."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"name": "Widget"}  # price is missing
        )
        assert response.status_code == 400

    def test_update_negative_price(self, admin_token, seed_product):
        """400 when price is below minimum (0)."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"name": "Widget", "price": -5.00}
        )
        assert response.status_code == 400

    @pytest.mark.parametrize("name", [
        "",                    # Below minLength (0 chars)
        "A" * 201,            # Above maxLength (201 chars)
    ])
    def test_update_name_boundary(self, admin_token, seed_product, name):
        """400 when name violates length constraints."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"name": name, "price": 10.00}
        )
        assert response.status_code == 400

    @pytest.mark.parametrize("name", [
        "A",                  # At minLength (1 char)
        "A" * 200,            # At maxLength (200 chars)
        "Normal Product",     # Nominal value
    ])
    def test_update_name_valid_boundaries(self, admin_token, seed_product, name):
        """200 when name is within valid length range."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"name": name, "price": 10.00}
        )
        assert response.status_code == 200

    def test_update_invalid_category_enum(self, admin_token, seed_product):
        """400 when category is not in the allowed enum."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"name": "Widget", "price": 10.00, "category": "weapons"}
        )
        assert response.status_code == 400

    @pytest.mark.parametrize("category", [
        "electronics", "clothing", "food", "other"
    ])
    def test_update_valid_category_values(self, admin_token, seed_product, category):
        """200 for each valid enum value."""
        response = httpx.put(
            f"{BASE_URL}/api/v2/products/{seed_product['id']}",
            headers={"Authorization": f"Bearer {admin_token}"},
            json={"name": "Widget", "price": 10.00, "category": category}
        )
        assert response.status_code == 200
        assert response.json()["category"] == category

Аннотация: что делает эти тесты хорошими

1. Настройка и очистка на основе фикстур

Фикстура seed_product создаёт тестовые данные и очищает их после завершения. Это обеспечивает изоляцию тестов -- ни один тест не зависит от состояния предыдущего.

2. Многоуровневые утверждения

Хорошие тесты проверяют не только код статуса:

# Not just: assert response.status_code == 200
# But also:
assert body["id"] == seed_product["id"]    # Correct resource returned
assert body["category"] in [...]           # Valid enum
assert body["price"] >= 0                  # Business constraint

3. Параметризованные граничные тесты

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

4. Проверка валидных и невалидных границ

Обратите внимание на ДВА параметризованных теста для длины имени:

  • test_update_name_boundary тестирует НЕВАЛИДНЫЕ значения (ожидается 400)
  • test_update_name_valid_boundaries тестирует ВАЛИДНЫЕ значения (ожидается 200)

Это обеспечивает покрытие обеих сторон границы.

5. Каждый документированный код статуса имеет тест

Схема документирует 200, 400, 401 и 403. Для каждого есть хотя бы один тест, вызывающий этот код.

Типичные улучшения после генерации

Проблема Как исправить
BASE_URL захардкожен Вынести в переменную окружения или фикстуру pytest
get_test_token не определён Создать фикстуру conftest.py или вспомогательный модуль
Отсутствует таймаут в HTTP-вызовах Добавить timeout=10.0 во все вызовы httpx
Нет утверждений о сообщениях ошибок Добавить assert "name" in response.json()["detail"] для ответов 400
Отсутствует заголовок Content-Type Добавить "Content-Type": "application/json" в заголовки
Нет теста на семантику PATCH vs PUT Если оба существуют, проверить что PUT заменяет, а PATCH обновляет

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

Тесты API, сгенерированные ИИ из OpenAPI-схем, получаются на удивление качественными, если промпт включает полную схему с разрешёнными $ref, указывает фреймворк и механизм аутентификации, и запрашивает конкретные категории тестов (happy path, ошибки, границы, аутентификация). Основная задача при ревью -- убедиться, что фикстуры и хелперы существуют в вашей кодовой базе и что утверждения о сообщениях ошибок достаточно специфичны.