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

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

@potapov_me

Платформа

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

Контент

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

Компания

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

Аккаунт

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

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

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. TDD Практика: Premium-подписка (Часть 1)
tdd_premium_subscription_part1

TDD Практика: Premium-подписка (Часть 1)

Добавляем Premium-подписку в Todo через TDD. Тесты для бизнес-логики, модели, первые failing tests

Добавим Premium-подписку в Todo-приложение через TDD. Часть 1: бизнес-логика и модели.

Цель: Применить TDD для добавления новой фичи от начала до конца.

Требования: Premium-подписка

### User Story Как пользователь Todo, Я хочу Premium-подписку, Чтобы создавать неограниченное количество задач ### Acceptance Criteria 1. Free пользователи: максимум 10 задач 2. Premium пользователи: неограниченно 3. Premium = $9.99/month или $99/year (скидка 17%) 4. Подписка активируется после успешного платежа 5. Подписка истекает через 30 дней (monthly) или 365 дней (annual)

Итерация 1: User model

RED: Тест для User с подпиской

# tests/test_user_subscription.py import pytest from datetime import datetime, timedelta from todo_app.models import User, SubscriptionPlan def test_create_user_with_premium(): """RED: User ещё не поддерживает подписку""" user = User( email="user@test.com", subscription_plan=SubscriptionPlan.PREMIUM_MONTHLY ) assert user.email == "user@test.com" assert user.subscription_plan == SubscriptionPlan.PREMIUM_MONTHLY

❌ NameError: name 'SubscriptionPlan' is not defined

GREEN: Минимальная модель

# todo_app/models.py from enum import Enum from dataclasses import dataclass from datetime import datetime from typing import Optional class SubscriptionPlan(Enum): FREE = "free" PREMIUM_MONTHLY = "premium_monthly" PREMIUM_ANNUAL = "premium_annual" @dataclass class User: email: str subscription_plan: SubscriptionPlan = SubscriptionPlan.FREE subscription_expires_at: Optional[datetime] = None

✅ PASSED

Итерация 2: is_premium метод

RED

def test_user_is_premium_when_subscribed(): """RED: Метод is_premium ещё не существует""" user = User( email="premium@test.com", subscription_plan=SubscriptionPlan.PREMIUM_MONTHLY, subscription_expires_at=datetime.now() + timedelta(days=10) ) assert user.is_premium() is True def test_user_is_not_premium_when_free(): """Свободный пользователь""" user = User(email="free@test.com") assert user.is_premium() is False

❌ AttributeError: 'User' object has no attribute 'is_premium'

GREEN

@dataclass class User: email: str subscription_plan: SubscriptionPlan = SubscriptionPlan.FREE subscription_expires_at: Optional[datetime] = None def is_premium(self) -> bool: """Проверка Premium-статуса""" if self.subscription_plan == SubscriptionPlan.FREE: return False # Проверка срока действия if self.subscription_expires_at is None: return False return datetime.now() < self.subscription_expires_at

✅ PASSED

Итерация 3: Expired subscription

RED

def test_user_is_not_premium_when_subscription_expired(): """RED: Истекшая подписка""" user = User( email="expired@test.com", subscription_plan=SubscriptionPlan.PREMIUM_MONTHLY, subscription_expires_at=datetime.now() - timedelta(days=1) # Истекла вчера ) assert user.is_premium() is False

✅ PASSED (код уже обрабатывает это!)

Итерация 4: Todo limits

RED

# tests/test_todo_limits.py def test_free_user_can_create_up_to_10_todos(): """RED: Лимит для free пользователей""" user = User(email="free@test.com") todos = [] # Создать 10 задач (должно работать) for i in range(10): todos.append(TodoItem(title=f"Task {i+1}", user=user)) # 11-я задача должна упасть with pytest.raises(TodoLimitExceeded): TodoItem(title="Task 11", user=user) def test_premium_user_can_create_unlimited_todos(): """Premium пользователи без лимита""" user = User( email="premium@test.com", subscription_plan=SubscriptionPlan.PREMIUM_MONTHLY, subscription_expires_at=datetime.now() + timedelta(days=10) ) # Создать 100 задач (должно работать) for i in range(100): TodoItem(title=f"Task {i+1}", user=user) # Не должно быть ошибки

❌ NameError: name 'TodoLimitExceeded' is not defined

GREEN

# todo_app/exceptions.py class TodoLimitExceeded(Exception): """Превышен лимит задач""" pass # todo_app/models.py @dataclass class TodoItem: title: str user: User priority: Priority = Priority.MEDIUM def __post_init__(self): """Проверка лимита при создании""" if not self.user.can_create_todo(): raise TodoLimitExceeded( f"User {self.user.email} reached todo limit" ) @dataclass class User: # ... def can_create_todo(self) -> bool: """Проверка лимита задач""" if self.is_premium(): return True # Unlimited # Free: максимум 10 задач # TODO: Нужен TodoRepository для подсчёта return True # Временно

⚠️ Проблема: Нужен TodoRepository для подсчёта существующих задач.

REFACTOR: Dependency Injection

# todo_app/repositories.py from abc import ABC, abstractmethod class TodoRepository(ABC): @abstractmethod def count_user_todos(self, user_id: str) -> int: pass # todo_app/services.py class TodoService: def __init__(self, todo_repo: TodoRepository): self.repo = todo_repo def create_todo(self, user: User, title: str, priority: Priority) -> TodoItem: """Создать задачу с проверкой лимита""" if not user.is_premium(): existing_count = self.repo.count_user_todos(user.email) if existing_count >= 10: raise TodoLimitExceeded( f"Free users can only have 10 todos. Upgrade to Premium!" ) return TodoItem(title=title, user=user, priority=priority)

Обновить тест

def test_free_user_limit_with_repository(): """Тест с mock repository""" mock_repo = Mock() mock_repo.count_user_todos.return_value = 10 # У пользователя уже 10 задач user = User(email="free@test.com") service = TodoService(mock_repo) # 11-я задача должна упасть with pytest.raises(TodoLimitExceeded): service.create_todo(user, "Task 11", Priority.HIGH)

✅ PASSED

Итерация 5: Subscription pricing

RED

# tests/test_subscription_pricing.py def test_premium_monthly_price(): """RED: Цены ещё не определены""" price = SubscriptionPlan.PREMIUM_MONTHLY.get_price() assert price == 9.99 def test_premium_annual_price(): """Annual со скидкой""" price = SubscriptionPlan.PREMIUM_ANNUAL.get_price() assert price == 99.00 # $9.99 * 12 * 0.83 ≈ $99

❌ AttributeError: 'SubscriptionPlan' object has no attribute 'get_price'

GREEN

class SubscriptionPlan(Enum): FREE = ("free", 0.0) PREMIUM_MONTHLY = ("premium_monthly", 9.99) PREMIUM_ANNUAL = ("premium_annual", 99.00) def __init__(self, plan_id, price): self.plan_id = plan_id self.price = price def get_price(self) -> float: return self.price

✅ PASSED

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

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

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

Далее: TDD Практика: Premium-подписка (Часть 2)