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

Легче ли тестировать код с Dependency Injection

2.0 Middle🔥 241 комментариев
#Тестирование#Архитектура и паттерны

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

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

Легче ли тестировать код с Dependency Injection (DI)

Ответ: Да, значительно легче. Dependency Injection делает тестирование проще, быстрее и надёжнее. Это один из главных паттернов для написания тестируемого кода.

Проблема без Dependency Injection

Тесный связанный код (плохо)

# БЕЗ Dependency Injection — тесный coupling
class EmailService:
    def send(self, to, message):
        # Реальное отправление письма
        import smtplib
        # ...
        print(f"Email sent to {to}")

class UserRegistration:
    def __init__(self):
        # Сервис жёстко вбит в класс
        self.email_service = EmailService()  # ❌ Зависимость создана здесь
    
    def register(self, email, password):
        # ... валидация ...
        self.email_service.send(email, "Welcome!")  # Реально отправляет письма
        return True

# Проблемы при тестировании:
class TestUserRegistration:
    def test_register(self):
        registration = UserRegistration()
        # ❌ При тестировании РЕАЛЬНО отправляется письмо!
        # ❌ Тест зависит от SMTP сервера
        # ❌ Тест медленный (1-2 секунды за письмо)
        # ❌ Может провалиться из-за сети
        result = registration.register("test@example.com", "password123")
        assert result == True

Проблемы:

  • Тест отправляет реальные письма
  • Тест зависит от интернета и SMTP сервера
  • Тест медленный
  • Нельзя проверить ошибки (что если SMTP упадёт?)
  • Нарушаем рабочую окружающую среду

Решение: Dependency Injection

С Dependency Injection (хорошо)

# С Dependency Injection
class EmailService:
    def send(self, to, message):
        import smtplib
        # Реальное отправление
        print(f"Email sent to {to}")

class UserRegistration:
    def __init__(self, email_service):  # ✓ Зависимость инъектируется
        self.email_service = email_service
    
    def register(self, email, password):
        if not email or not password:
            return False
        self.email_service.send(email, "Welcome!")  # Использует переданный сервис
        return True

# Тестирование с mock-объектом
class MockEmailService:
    """Фальшивый сервис для тестов"""
    def __init__(self):
        self.sent_emails = []
    
    def send(self, to, message):
        # Не отправляет реальные письма, только записывает
        self.sent_emails.append({"to": to, "message": message})

class TestUserRegistration:
    def test_register_success(self):
        # ✓ Используем mock вместо реального сервиса
        mock_email = MockEmailService()
        registration = UserRegistration(mock_email)
        
        result = registration.register("test@example.com", "pass123")
        
        assert result == True
        assert len(mock_email.sent_emails) == 1
        assert mock_email.sent_emails[0]["to"] == "test@example.com"
    
    def test_register_invalid_email(self):
        mock_email = MockEmailService()
        registration = UserRegistration(mock_email)
        
        result = registration.register("", "pass123")
        
        assert result == False
        assert len(mock_email.sent_emails) == 0  # Письмо не отправлено
    
    def test_register_with_real_service(self):
        # ✓ Можно тестировать и с реальным сервисом если нужно
        real_email = EmailService()
        registration = UserRegistration(real_email)
        
        # ...

Преимущества:

  • Тесты НЕ отправляют реальные письма
  • Тесты быстрые (миллисекунды вместо секунд)
  • Тесты независимы от внешних сервисов
  • Легко тестировать ошибки

Практический пример: Заказы и платежи

БЕЗ DI (плохо)

import stripe  # Реальный API платежей

class OrderProcessor:
    def __init__(self):
        self.payment_gateway = stripe.Charge()  # ❌ Жёстко вбито
    
    def process_order(self, order_id, amount):
        # При тесте РЕАЛЬНО попытается снять деньги!
        try:
            charge = self.payment_gateway.create(
                amount=amount * 100,
                currency='usd'
            )
            return {'success': True, 'charge_id': charge.id}
        except stripe.error.CardError:
            return {'success': False, 'error': 'Card declined'}

class TestOrderProcessor:
    def test_process_order(self):
        processor = OrderProcessor()
        # ❌ При тесте попытается снять РЕАЛЬНЫЕ ДЕНЬГИ!
        result = processor.process_order(123, 99.99)

С DI (хорошо)

from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    """Интерфейс для платёжного шлюза"""
    @abstractmethod
    def charge(self, amount, currency='usd'):
        pass

class StripePaymentGateway(PaymentGateway):
    """Реальная реализация со Stripe"""
    import stripe
    
    def charge(self, amount, currency='usd'):
        charge = stripe.Charge.create(
            amount=int(amount * 100),
            currency=currency
        )
        return {'success': True, 'charge_id': charge.id}

class MockPaymentGateway(PaymentGateway):
    """Mock для тестирования"""
    def __init__(self):
        self.charges = []
        self.should_fail = False
    
    def charge(self, amount, currency='usd'):
        if self.should_fail:
            return {'success': False, 'error': 'Card declined'}
        
        charge_id = f"charge_{len(self.charges) + 1}"
        self.charges.append({'amount': amount, 'currency': currency})
        return {'success': True, 'charge_id': charge_id}

class OrderProcessor:
    def __init__(self, payment_gateway: PaymentGateway):  # ✓ DI
        self.payment_gateway = payment_gateway
    
    def process_order(self, order_id, amount):
        result = self.payment_gateway.charge(amount)
        return result

# Тестирование
class TestOrderProcessor:
    def test_successful_payment(self):
        mock_gateway = MockPaymentGateway()
        processor = OrderProcessor(mock_gateway)
        
        result = processor.process_order(123, 99.99)
        
        assert result['success'] == True
        assert len(mock_gateway.charges) == 1
    
    def test_payment_declined(self):
        mock_gateway = MockPaymentGateway()
        mock_gateway.should_fail = True
        processor = OrderProcessor(mock_gateway)
        
        result = processor.process_order(123, 99.99)
        
        assert result['success'] == False
        assert result['error'] == 'Card declined'
    
    def test_with_real_stripe(self):
        # ✓ Можем использовать реальный Stripe, если нужно
        real_gateway = StripePaymentGateway()
        processor = OrderProcessor(real_gateway)
        # ...

Различные способы инъекции

1. Инъекция через конструктор (наиболее распространённо)

class UserService:
    def __init__(self, database, email_service):  # DI через конструктор
        self.database = database
        self.email_service = email_service

# Использование
db = DatabaseConnection()
email = EmailService()
user_service = UserService(db, email)

2. Инъекция через метод

class ReportGenerator:
    def generate(self, data_provider, formatter):  # DI через параметры
        data = data_provider.get_data()
        return formatter.format(data)

# Использование
generator = ReportGenerator()
report = generator.generate(real_provider, json_formatter)

3. Инъекция через property

class Logger:
    def __init__(self):
        self._output = None
    
    @property
    def output(self):
        return self._output
    
    @output.setter
    def output(self, value):  # DI через property
        self._output = value

# Использование
logger = Logger()
logger.output = ConsoleOutput()  # Инъекция

Использование pytest с fixtures

import pytest
from unittest.mock import Mock

class NotificationService:
    def __init__(self, email_service, sms_service):
        self.email_service = email_service
        self.sms_service = sms_service
    
    def notify_user(self, user_id, message):
        user = self._get_user(user_id)  # Допустим
        self.email_service.send(user['email'], message)
        if user.get('phone'):
            self.sms_service.send(user['phone'], message)

# Fixtures для тестов
@pytest.fixture
def mock_email_service():
    return Mock()  # Автоматический mock

@pytest.fixture
def mock_sms_service():
    return Mock()

@pytest.fixture
def notification_service(mock_email_service, mock_sms_service):
    return NotificationService(mock_email_service, mock_sms_service)

# Тесты с DI
class TestNotificationService:
    def test_notify_with_email(self, notification_service, mock_email_service):
        notification_service.notify_user(1, "Hello")
        mock_email_service.send.assert_called_once()
    
    def test_notify_with_sms(self, notification_service, mock_sms_service):
        notification_service.notify_user(2, "Alert")
        mock_sms_service.send.assert_called()

Фреймворки с встроенной DI

# FastAPI с встроенной DI
from fastapi import FastAPI, Depends

class Database:
    def query(self, sql):
        # ...
        pass

app = FastAPI()

# DI через Depends
def get_database() -> Database:
    return Database()  # Или из конфига

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: Database = Depends(get_database)):
    # db автоматически инъектируется
    return db.query(f"SELECT * FROM users WHERE id = {user_id}")

# Тестирование
def test_get_user():
    mock_db = Mock()
    mock_db.query.return_value = {'id': 1, 'name': 'John'}
    
    # Переопределяем зависимость
    app.dependency_overrides[get_database] = lambda: mock_db
    
    # Теперь при запросе используется mock

Сравнительная таблица

Характеристика      | Без DI    | С DI
                    |           |
————————————————————|———————————|——————
Тестируемость       | ❌ Низкая | ✓ Высокая
Тестовая скорость   | ❌ Медленно| ✓ Быстро
Изоляция тестов    | ❌ Плохая | ✓ Хорошая
Зависит от сетей   | ❌ Да    | ✓ Нет
Можно мокировать   | ❌ Сложно | ✓ Легко
Код переиспользуемо| ❌ Нет   | ✓ Да
Сложность кода     | ❌ Выше  | ✓ Ниже

Реальный пример: система заказов

from abc import ABC, abstractmethod
from typing import List

class Database(ABC):
    @abstractmethod
    def save_order(self, order):
        pass

class EmailNotifier(ABC):
    @abstractmethod
    def send(self, email, subject, body):
        pass

class OrderService:
    def __init__(self, database: Database, notifier: EmailNotifier):
        self.database = database
        self.notifier = notifier
    
    def create_order(self, customer_email, items: List[dict]) -> dict:
        order = {
            'items': items,
            'total': sum(item['price'] for item in items),
            'status': 'pending'
        }
        
        self.database.save_order(order)
        self.notifier.send(
            customer_email,
            "Order Confirmed",
            f"Your order total: ${order['total']}"
        )
        
        return order

# Тестирование
class MockDatabase(Database):
    def __init__(self):
        self.orders = []
    
    def save_order(self, order):
        self.orders.append(order)

class MockNotifier(EmailNotifier):
    def __init__(self):
        self.emails = []
    
    def send(self, email, subject, body):
        self.emails.append({'to': email, 'subject': subject})

def test_create_order():
    mock_db = MockDatabase()
    mock_notifier = MockNotifier()
    service = OrderService(mock_db, mock_notifier)
    
    items = [{'name': 'Book', 'price': 10}, {'name': 'Pen', 'price': 5}]
    order = service.create_order('user@example.com', items)
    
    assert order['total'] == 15
    assert len(mock_db.orders) == 1
    assert len(mock_notifier.emails) == 1
    assert mock_notifier.emails[0]['to'] == 'user@example.com'

Заключение

Да, с Dependency Injection НАМНОГО легче тестировать:

  1. Изоляция — каждый компонент тестируется отдельно
  2. Скорость — мок-объекты быстрее реальных
  3. Надёжность — не зависит от внешних сервисов
  4. Flexibility — легко подставить разные реализации
  5. Читаемость — явные зависимости легче понять

DI — это не просто паттерн дизайна, это фундамент для написания тестируемого, поддерживаемого и масштабируемого кода. Все современные фреймворки (FastAPI, Django, Flask с blueprints) его поддерживают.