📌 D – Dependency Inversion Principle (DIP)
Принцип инверсии зависимостей
“Зависимости должны быть от абстракций, а не от конкретных реализаций.”
В DIP помогает строить гибкие, легко тестируемые и переиспользуемые компоненты.
Особенно когда нужно подменить авторизацию, логирование, сделать allure-разметку или сбор coverage
В чём суть?
Класс не должен напрямую зависеть от конкретной реализации, он должен зависеть от интерфейса (абстракции).
🧪 Пример: плохая реализация (без DIP)
Скажу честно, сам я тоже когда-то грешил завязываясь на конкретную реализацию клиента, в данном случае requests.Session()
class ApiClient:
def __init__(self):
self.session = requests.Session()
def get_user(
self,
user_id
) -> dict:
response = self.session.get(
f"{API_URL}/user/{user_id}"
)
return response.json()
Проблемы:
• Жёсткая зависимость от requests, если мы захотим использовать, например httpx, мы не сможем этого сделать без изменения кода.
• Нельзя замокать поведение в тесте
• Нарушен DIP: клиент зависит от деталей
✅ Как сделать правильно (с применением DIP)
То есть говорим какие методы обязательно должны быть у принимаемого на вход клиента.
В Python нет интерфейсов в классическом понимании, поэтому
abc.ABC и @abstractmethod — это абстрактные классы, которые эмулируют интерфейсы.1. Вводим интерфейсы:
from abc import ABC, abstractmethod
class BaseHttpClient(ABC):
@abstractmethod
def get(
self,
url: str,
**kwargs: Any
) -> dict:
pass
2. Реализация:
Наследуемся и описываем методы соблюдая контракт, "контракт" — это обязательство реализовать методы, объявленные в абстракции (
BaseHttpClient).При соблюдении этих правил, любой клиент, который соблюдает контракт, может быть безболезнено (в идеале) использован.
class HttpClient(BaseHttpClient):
def get(
self,
url: str,
**kwargs: Any
) -> dict:
return requests.get(url, **kwargs).json()
3. Внедряем зависимости:
То есть любой наследник BaseHttpClient не должен поломать код.
class ApiClient:
def __init__(
self,
http_client: BaseHttpClient
):
self.http_client = http_client
def get_user(self, user_id, headers) -> dict:
return self.http_client.get(
f"{API_URL}/user/{user_id}",
headers=headers,
).json()
🧪 Теперь в тесте:
# Мы можем или замокать клиент тем самым написать тест без реального апи
class MockHttpClient(BaseHttpClient):
def get(self, url, headers):
return {"id": 123, "name": "Test User"}
def test_get_user():
client = ApiClient(MockHttpClient())
user = client.get_user(123)
assert user["id"] == 123
# Можем добавить свою логику, например логгирование запроса
class LoggedHttpClient(BaseHttpClient):
def get(
self,
url,
headers
) -> dict:
print(url, headers)
response = self.http_client.get(
url,
headers=headers
)
print(response.text)
return response.json()
# И здесь тест уже будет с логами
def test_get_logged_user():
client = ApiClient(LoggedHttpClient())
user = client.get_user(123)
assert user["id"] == 123
Профит:
• Подменили зависимости без боли
• Не нужен реальный backend для тестов
• Код клиента не меняется — только зависимости
DIP особенно полезен когда:
• Пишешь интеграционные и контрактные тесты
• Нужно мокать внешние сервисы
• В проекте много обвязки (логов, ретраев, токенов) — и ты хочешь это всё отделить
• Требуется расширяемость (добавить кеш, валидацию, другой http-клиент)
Вывод
Применение DIP в API‑тестах:
🔹 Упрощает подмену зависимостей
🔹 Повышает читаемость и тестируемость
🔹 Позволяет изолировать бизнес‑логику от инфраструктуры
🔹 Снижает связанность компонентов
🔥Перешли другу\коллеге пусть тоже знает)
На этом мы закрываем тему SOLID. )
——————————-
📱 TG-сообщество
📱 Обучение
📱 Отзывы
