Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Практика: Покрываем модуль платежей
legacy_payments_practice

Практика: Покрываем модуль платежей

Берём модуль платежей Todo-приложения без тестов. Применяем все паттерны: characterization, seams, strangler fig

Практика: применим ВСЕ паттерны legacy-рефакторинга к реальному модулю платежей Todo-приложения.

Цель: Покрыть тестами legacy-модуль без тестов и подготовить к добавлению Premium-подписки.

Исходный Legacy-код

# todo_app/payments.py (LEGACY — БЕЗ ТЕСТОВ) import stripe import smtplib from datetime import datetime import psycopg2 stripe.api_key = "sk_live_abc123" class PaymentProcessor: """Legacy процессор платежей (написан 2 года назад)""" def process_payment(self, user_id, amount_cents, card_token): """Обработать платёж""" # Проверка пользователя в БД conn = psycopg2.connect("postgresql://prod:5432/tododb") cur = conn.cursor() cur.execute("SELECT email, name FROM users WHERE id=%s", (user_id,)) user = cur.fetchone() if not user: return {"status": "error", "message": "User not found"} email, name = user # Создание платежа в Stripe try: charge = stripe.Charge.create( amount=amount_cents, currency="usd", source=card_token, description=f"Todo Premium - {name}" ) except stripe.error.CardError: return {"status": "error", "message": "Card declined"} # Сохранение в БД cur.execute( "INSERT INTO payments (user_id, amount, status, created_at) VALUES (%s, %s, %s, %s)", (user_id, amount_cents, "completed", datetime.now()) ) conn.commit() # Отправка email smtp = smtplib.SMTP("smtp.gmail.com", 587) smtp.login("todo@app.com", "password123") message = f"Payment received: ${amount_cents/100:.2f}" smtp.send_message(message, from_addr="todo@app.com", to_addrs=[email]) return {"status": "success", "charge_id": charge.id}

Шаг 1: Characterization Tests

# tests/test_payments_characterization.py import pytest from unittest.mock import patch, Mock from todo_app.payments import PaymentProcessor @pytest.fixture def mock_dependencies(monkeypatch): """Mock всех внешних зависимостей""" # Mock PostgreSQL mock_conn = Mock() mock_cur = Mock() mock_cur.fetchone.return_value = ("user@test.com", "Test User") mock_conn.cursor.return_value = mock_cur monkeypatch.setattr("psycopg2.connect", lambda _: mock_conn) # Mock Stripe mock_charge = Mock() mock_charge.id = "ch_123" monkeypatch.setattr("stripe.Charge.create", lambda **kwargs: mock_charge) # Mock SMTP mock_smtp = Mock() monkeypatch.setattr("smtplib.SMTP", lambda host, port: mock_smtp) return { "conn": mock_conn, "cursor": mock_cur, "charge": mock_charge, "smtp": mock_smtp, } def test_successful_payment_characterization(mock_dependencies): """Characterization: успешный платёж""" processor = PaymentProcessor() result = processor.process_payment( user_id=1, amount_cents=2000, card_token="tok_visa" ) # Проверка текущего поведения assert result["status"] == "success" assert result["charge_id"] == "ch_123" # Проверка что БД вызвана mock_dependencies["cursor"].execute.assert_called() # Проверка что email отправлен mock_dependencies["smtp"].send_message.assert_called_once()

Шаг 2: Выделить Seams

# todo_app/payments_refactored.py from abc import ABC, abstractmethod from datetime import datetime from typing import Optional # Интерфейсы (Seams) class PaymentGateway(ABC): @abstractmethod def charge(self, amount_cents, card_token, description): pass class EmailService(ABC): @abstractmethod def send_payment_confirmation(self, email, amount_cents): pass class UserRepository(ABC): @abstractmethod def get_user(self, user_id): pass @abstractmethod def save_payment(self, user_id, amount_cents, status, timestamp): pass # Реализации class StripeGateway(PaymentGateway): def __init__(self, api_key): import stripe stripe.api_key = api_key def charge(self, amount_cents, card_token, description): import stripe try: charge = stripe.Charge.create( amount=amount_cents, currency="usd", source=card_token, description=description ) return {"status": "success", "charge_id": charge.id} except stripe.error.CardError: return {"status": "error", "message": "Card declined"} class SMTPEmailService(EmailService): def __init__(self, smtp_host, smtp_user, smtp_password): self.host = smtp_host self.user = smtp_user self.password = smtp_password def send_payment_confirmation(self, email, amount_cents): import smtplib smtp = smtplib.SMTP(self.host, 587) smtp.login(self.user, self.password) message = f"Payment received: ${amount_cents/100:.2f}" smtp.send_message(message, from_addr=self.user, to_addrs=[email]) class PostgreSQLUserRepository(UserRepository): def __init__(self, connection_string): import psycopg2 self.conn = psycopg2.connect(connection_string) def get_user(self, user_id): cur = self.conn.cursor() cur.execute("SELECT email, name FROM users WHERE id=%s", (user_id,)) return cur.fetchone() def save_payment(self, user_id, amount_cents, status, timestamp): cur = self.conn.cursor() cur.execute( "INSERT INTO payments (user_id, amount, status, created_at) VALUES (%s, %s, %s, %s)", (user_id, amount_cents, status, timestamp) ) self.conn.commit() # Новый PaymentProcessor (тестируемый) class PaymentProcessorV2: def __init__( self, payment_gateway: PaymentGateway, email_service: EmailService, user_repository: UserRepository, current_time_func=None ): self.payment_gateway = payment_gateway self.email_service = email_service self.user_repository = user_repository self.current_time = current_time_func or datetime.now def process_payment(self, user_id, amount_cents, card_token): """Обработать платёж (тестируемая версия)""" # Получить пользователя user = self.user_repository.get_user(user_id) if not user: return {"status": "error", "message": "User not found"} email, name = user # Создать платёж charge_result = self.payment_gateway.charge( amount_cents=amount_cents, card_token=card_token, description=f"Todo Premium - {name}" ) if charge_result["status"] == "error": return charge_result # Сохранить в БД self.user_repository.save_payment( user_id=user_id, amount_cents=amount_cents, status="completed", timestamp=self.current_time() ) # Отправить email self.email_service.send_payment_confirmation(email, amount_cents) return charge_result

Шаг 3: Тесты нового кода

# tests/test_payments_v2.py from unittest.mock import Mock from datetime import datetime from todo_app.payments_refactored import PaymentProcessorV2 def test_successful_payment_v2(): """Тест нового кода с моками""" # Mock зависимостей mock_gateway = Mock() mock_gateway.charge.return_value = {"status": "success", "charge_id": "ch_123"} mock_email = Mock() mock_repo = Mock() mock_repo.get_user.return_value = ("user@test.com", "Test User") fixed_time = datetime(2025, 1, 1, 12, 0, 0) # Создать processor processor = PaymentProcessorV2( payment_gateway=mock_gateway, email_service=mock_email, user_repository=mock_repo, current_time_func=lambda: fixed_time ) # Выполнить result = processor.process_payment(1, 2000, "tok_visa") # Проверки assert result["status"] == "success" assert result["charge_id"] == "ch_123" mock_gateway.charge.assert_called_once_with( amount_cents=2000, card_token="tok_visa", description="Todo Premium - Test User" ) mock_repo.save_payment.assert_called_once_with( user_id=1, amount_cents=2000, status="completed", timestamp=fixed_time ) mock_email.send_payment_confirmation.assert_called_once_with("user@test.com", 2000)

Шаг 4: Facade с Feature Flag

# todo_app/payments_facade.py import os from todo_app.payments import PaymentProcessor as LegacyProcessor from todo_app.payments_refactored import ( PaymentProcessorV2, StripeGateway, SMTPEmailService, PostgreSQLUserRepository ) USE_V2 = os.getenv("USE_PAYMENT_V2", "false") == "true" # Инициализация V2 if USE_V2: gateway = StripeGateway(api_key=os.getenv("STRIPE_API_KEY")) email_service = SMTPEmailService( smtp_host="smtp.gmail.com", smtp_user=os.getenv("SMTP_USER"), smtp_password=os.getenv("SMTP_PASSWORD") ) user_repo = PostgreSQLUserRepository( connection_string=os.getenv("DATABASE_URL") ) processor_v2 = PaymentProcessorV2(gateway, email_service, user_repo) def process_payment(user_id, amount_cents, card_token): """Facade: роутинг между legacy и V2""" if USE_V2: return processor_v2.process_payment(user_id, amount_cents, card_token) else: legacy = LegacyProcessor() return legacy.process_payment(user_id, amount_cents, card_token)

Шаг 5: Rollout Plan

### Week 1-2: Подготовка - [x] Написать characterization tests для legacy - [x] Рефакторить V2 с seams - [x] Написать тесты V2 - [x] Code review ### Week 3: Shadow Mode ```python # Оба кода работают, сравниваем результаты legacy_result = legacy_processor.process_payment(...) v2_result = processor_v2.process_payment(...) if legacy_result != v2_result: logger.warning("Mismatch detected!") return legacy_result # Пока используем legacy ```

Week 4: 10% rollout

  • USE_V2=true для 10% пользователей
  • Мониторинг ошибок

Week 5: 50% rollout

  • USE_V2=true для 50%

Week 6: 100% rollout

  • USE_V2=true для всех
  • Удалить legacy код через 2 недели

Проверьте свои знания

Вопросы ещё не добавлены

Вопросы для этой подтемы ещё не добавлены.

Далее: TDD: Red-Green-Refactor цикл в деталях