← Назад к вопросам

Какими классами описал бы CRUD при тестировании API

2.0 Middle🔥 121 комментариев
#Soft skills и карьера#Теория тестирования

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура классов для тестирования CRUD API

При проектировании фреймворка для тестирования CRUD API я бы создал следующую иерархию классов, основанную на принципах Page Object Model (POM) и Layered Architecture. Такой подход обеспечивает поддерживаемость, переиспользование кода и чёткое разделение ответственности.

Основные слои архитектуры

1. Базовый слой (Core Layer)

# base_api_client.py
import requests
from abc import ABC, abstractmethod

class BaseApiClient(ABC):
    """Базовый клиент для всех API-запросов"""
    
    def __init__(self, base_url: str, timeout: int = 30):
        self.base_url = base_url
        self.timeout = timeout
        self.session = requests.Session()
        self._setup_session()
    
    def _setup_session(self):
        """Настройка сессии с заголовками и аутентификацией"""
        self.session.headers.update({
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        })
    
    def _send_request(self, method: str, endpoint: str, **kwargs):
        """Единый метод для отправки HTTP-запросов"""
        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        response = self.session.request(
            method=method,
            url=url,
            timeout=self.timeout,
            **kwargs
        )
        return ApiResponse(response)

2. Слой работы с ответами (Response Layer)

# api_response.py
from dataclasses import dataclass
from typing import Any, Optional

@dataclass
class ApiResponse:
    """Класс-обёртка для стандартизации работы с ответами"""
    raw_response: Any
    status_code: int
    headers: dict
    data: Optional[Any] = None
    execution_time: Optional[float] = None
    
    def __init__(self, response):
        self.raw_response = response
        self.status_code = response.status_code
        self.headers = dict(response.headers)
        
        try:
            self.data = response.json()
        except ValueError:
            self.data = response.text
    
    def assert_status_code(self, expected_code: int):
        """Валидация статус-кода"""
        assert self.status_code == expected_code, \
            f"Expected {expected_code}, got {self.status_code}"
        return self
    
    def assert_schema(self, schema: dict):
        """Валидация структуры ответа"""
        # Здесь можно использовать библиотеку типа jsonschema
        pass

3. Слой моделей (Models Layer)

# models.py
from dataclasses import dataclass
from typing import Optional
from datetime import datetime

@dataclass
class User:
    """Модель данных пользователя"""
    id: Optional[int] = None
    name: str = ""
    email: str = ""
    created_at: Optional[datetime] = None
    
    def to_dict(self) -> dict:
        """Конвертация в словарь для отправки в API"""
        data = {
            'name': self.name,
            'email': self.email
        }
        if self.id:
            data['id'] = self.id
        return data

4. CRUD сервисный слой (Service Layer)

# crud_service.py
from typing import TypeVar, Generic, Optional

T = TypeVar('T')  # Generic type для моделей

class CrudService(Generic[T]):
    """Абстрактный класс для CRUD-операций"""
    
    def __init__(self, api_client: BaseApiClient, endpoint: str):
        self.client = api_client
        self.endpoint = endpoint
    
    def create(self, entity: T) -> ApiResponse:
        """Создание новой сущности"""
        response = self.client._send_request(
            'POST',
            self.endpoint,
            json=entity.to_dict()
        )
        if response.status_code == 201:
            # Обновляем ID созданной сущности
            entity.id = response.data.get('id')
        return response
    
    def read(self, entity_id: int) -> ApiResponse:
        """Чтение сущности по ID"""
        return self.client._send_request(
            'GET',
            f"{self.endpoint}/{entity_id}"
        )
    
    def update(self, entity_id: int, entity: T) -> ApiResponse:
        """Полное обновление сущности"""
        return self.client._send_request(
            'PUT',
            f"{self.endpoint}/{entity_id}",
            json=entity.to_dict()
        )
    
    def delete(self, entity_id: int) -> ApiResponse:
        """Удаление сущности"""
        return self.client._send_request(
            'DELETE',
            f"{self.endpoint}/{entity_id}"
        )
    
    def list_all(self, params: Optional[dict] = None) -> ApiResponse:
        """Получение списка всех сущностей"""
        return self.client._send_request(
            'GET',
            self.endpoint,
            params=params
        )

5. Конкретные сервисы (Concrete Services)

# user_service.py
class UserService(CrudService[User]):
    """Сервис для работы с пользователями"""
    
    def __init__(self, api_client: BaseApiClient):
        super().__init__(api_client, endpoint='/users')
    
    def search_by_email(self, email: str) -> ApiResponse:
        """Поиск пользователя по email"""
        return self.client._send_request(
            'GET',
            self.endpoint,
            params={'email': email}
        )
    
    def partial_update(self, user_id: int, updates: dict) -> ApiResponse:
        """Частичное обновление пользователя (PATCH)"""
        return self.client._send_request(
            'PATCH',
            f"{self.endpoint}/{user_id}",
            json=updates
        )

6. Слой тестов (Tests Layer)

# test_user_crud.py
import pytest

class TestUserCRUD:
    """Тесты для CRUD операций пользователей"""
    
    @pytest.fixture
    def user_service(self, api_client):
        return UserService(api_client)
    
    def test_create_user(self, user_service):
        """Тест создания пользователя"""
        user = User(name="John Doe", email="john@example.com")
        response = user_service.create(user)
        
        response.assert_status_code(201)
        assert response.data['name'] == user.name
        assert response.data['email'] == user.email
        assert 'id' in response.data
    
    def test_read_user(self, user_service, created_user):
        """Тест чтения пользователя"""
        response = user_service.read(created_user.id)
        
        response.assert_status_code(200)
        assert response.data['id'] == created_user.id

Ключевые преимущества данной архитектуры:

  • Инкапсуляция логики: Каждый класс отвечает за конкретную задачу
  • Переиспользование кода: Базовые классы могут использоваться для разных endpoint'ов
  • Поддержка различных протоколов: Можно легко добавить поддержку gRPC, GraphQL
  • Гибкость валидаций: Расширяемая система проверок ответов
  • Лёгкость миграции: Простота перехода на другой HTTP-клиент
  • Поддержка разных форматов данных: JSON, XML, Protobuf через адаптеры

Такой подход позволяет масштабировать тестовый фреймворк по мере роста проекта и обеспечивает чёткое разделение между бизнес-логикой тестов и техническими деталями реализации HTTP-запросов.