← Назад к вопросам
Какими классами описал бы 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-запросов.