Как внедрить тесты в непроверяемый код. Dependency injection для legacy. Швы и точки расширения
Seam (шов) — место в коде, где можно изменить поведение без изменения самого кода. Это позволяет внедрить тесты в непроверяемый legacy-код.
Цель: Научиться находить и создавать швы для тестирования legacy-кода.
# legacy/order.py
import smtplib
from datetime import datetime
class OrderProcessor:
def process_order(self, order_id, customer_email):
"""Обработать заказ (непроверяемый!)"""
# ❌ Прямой вызов БД
conn = psycopg2.connect("postgresql://prod:5432/db")
cur = conn.cursor()
cur.execute("UPDATE orders SET status='processed' WHERE id=%s", (order_id,))
conn.commit()
# ❌ Прямая отправка email
smtp = smtplib.SMTP("smtp.gmail.com", 587)
smtp.send_message(f"Order {order_id} processed", to=customer_email)
# ❌ Текущее время (недетерминированно)
timestamp = datetime.now()
# ❌ Внешний API
response = requests.post("https://analytics.com/track", json={"order": order_id})
return f"Processed at {timestamp}"Проблемы:
До (плохо):
class OrderProcessor:
def process_order(self, order_id):
# ❌ Hardcoded зависимость
conn = psycopg2.connect("postgresql://prod:5432/db")
# ...После (хорошо):
class OrderProcessor:
def __init__(self, db_connection):
# ✅ Зависимость внедряется извне
self.db = db_connection
def process_order(self, order_id):
cur = self.db.cursor()
cur.execute("UPDATE orders SET status='processed' WHERE id=%s", (order_id,))
self.db.commit()Тест:
def test_process_order():
"""Тест с mock БД"""
mock_db = Mock()
mock_cursor = Mock()
mock_db.cursor.return_value = mock_cursor
processor = OrderProcessor(mock_db)
processor.process_order(123)
mock_cursor.execute.assert_called_once_with(
"UPDATE orders SET status='processed' WHERE id=%s", (123,)
)До:
def process_order(self, order_id):
timestamp = datetime.now() # ❌ Недетерминированно
return f"Processed at {timestamp}"После:
def process_order(self, order_id, current_time=None):
if current_time is None:
current_time = datetime.now() # ✅ Default поведение
return f"Processed at {current_time}"Тест:
def test_process_order_timestamp():
"""Тест с фиксированным временем"""
fixed_time = datetime(2025, 1, 1, 12, 0, 0)
result = process_order(123, current_time=fixed_time)
assert result == "Processed at 2025-01-01 12:00:00"Legacy код (не меняем):
# legacy/notifier.py
import smtplib
def send_notification(email, message):
"""Отправить email (legacy)"""
smtp = smtplib.SMTP("smtp.gmail.com", 587)
smtp.send_message(message, to=email)Тест с monkey patch:
def test_send_notification(monkeypatch):
"""Тест с подменой smtplib"""
mock_smtp = Mock()
mock_smtp_instance = Mock()
mock_smtp.return_value = mock_smtp_instance
# Подмена модуля
monkeypatch.setattr("smtplib.SMTP", mock_smtp)
send_notification("test@test.com", "Hello")
# Проверка что SMTP вызван
mock_smtp.assert_called_once_with("smtp.gmail.com", 587)
mock_smtp_instance.send_message.assert_called_once()Legacy код:
import os
def get_api_key():
"""Получить API ключ из окружения"""
return os.getenv("API_KEY", "default-key")Тест:
def test_get_api_key(monkeypatch):
"""Тест с подменой окружения"""
monkeypatch.setenv("API_KEY", "test-key-123")
result = get_api_key()
assert result == "test-key-123"До:
class PaymentProcessor:
def charge(self, amount, card_number):
# ❌ Прямой вызов Stripe
stripe.Charge.create(amount=amount, source=card_number)После:
class PaymentProcessor:
def __init__(self, payment_gateway):
self.gateway = payment_gateway # ✅ DI
def charge(self, amount, card_number):
self.gateway.charge(amount, card_number)# interfaces/payment.py
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount, card_number):
pass
# Production implementation
class StripeGateway(PaymentGateway):
def charge(self, amount, card_number):
stripe.Charge.create(amount=amount, source=card_number)
# Test implementation
class MockGateway(PaymentGateway):
def __init__(self):
self.charges = []
def charge(self, amount, card_number):
self.charges.append({"amount": amount, "card": card_number})def test_payment_processing():
"""Тест с mock gateway"""
mock_gateway = MockGateway()
processor = PaymentProcessor(mock_gateway)
processor.charge(100.0, "4242424242424242")
assert len(mock_gateway.charges) == 1
assert mock_gateway.charges[0]["amount"] == 100.0# legacy/email_service.py
import smtplib
import logging
from datetime import datetime
class EmailService:
def send_welcome_email(self, user_email, user_name):
"""Отправить welcome email"""
# ❌ Hardcoded зависимости
smtp = smtplib.SMTP("smtp.gmail.com", 587)
logger = logging.getLogger()
message = f"Welcome {user_name}!"
timestamp = datetime.now()
try:
smtp.send_message(message, to=user_email)
logger.info(f"Email sent to {user_email} at {timestamp}")
return True
except Exception as e:
logger.error(f"Failed to send email: {e}")
return FalseЗадание: Рефакторить для тестируемости:
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Legacy Code: Strangler Fig Pattern