Добавляем Premium-подписку в Todo через TDD. Тесты для бизнес-логики, модели, первые failing tests
Добавим Premium-подписку в Todo-приложение через TDD. Часть 1: бизнес-логика и модели.
Цель: Применить TDD для добавления новой фичи от начала до конца.
### 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)# 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
# 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
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'
@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
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 (код уже обрабатывает это!)
# 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
# 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 для подсчёта существующих задач.
# 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
# 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'
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)