← Назад к вопросам
Как нужно писать функцию, чтобы ее было просто тестировать?
2.3 Middle🔥 151 комментариев
#Архитектура и паттерны#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Написание тестируемого кода
Код, который легко тестировать, обладает определёнными свойствами. Они вытекают из принципов SOLID, чистой архитектуры и здравого смысла.
1. Функция должна быть чистой (Pure Function)
Чистая функция:
- Возвращает один результат для одного входа
- Без побочных эффектов
- Не изменяет глобальное состояние
- Не зависит от времени выполнения
# ПЛОХО: нечистая функция
global_multiplier = 2
def calculate(x):
return x * global_multiplier # Зависит от глобального состояния!
# Сложно тестировать: нужно мокировать global_multiplier
# ХОРОШО: чистая функция
def calculate(x, multiplier):
return x * multiplier
# Легко тестировать
assert calculate(5, 2) == 10
assert calculate(5, 3) == 15
2. Функция должна иметь одну ответственность
# ПЛОХО: функция делает слишком много
def process_user_and_send_email(user_data):
# Валидация
if not user_data['email']:
raise ValueError("Email required")
# Преобразование
user = User(
name=user_data['name'],
email=user_data['email']
)
# Сохранение в БД
db.users.insert(user)
# Отправка email
send_email(user.email, "Welcome!")
return user
# Сложно тестировать: нужно мокировать БД, email, валидацию
# ХОРОШО: разделить на функции
def validate_user_data(user_data):
"""Валидация"""
if not user_data.get('email'):
raise ValueError("Email required")
if not user_data.get('name'):
raise ValueError("Name required")
def create_user(user_data):
"""Только создание объекта"""
return User(
name=user_data['name'],
email=user_data['email']
)
def process_user(user_data, user_repository, email_service):
"""Оркестрация"""
validate_user_data(user_data)
user = create_user(user_data)
user_repository.insert(user)
email_service.send_welcome(user.email)
return user
# Легко тестировать каждую функцию отдельно
3. Зависимости передавайте как параметры (Dependency Injection)
# ПЛОХО: зависимость жёстко закодирована
import requests
def get_user_data(user_id):
response = requests.get(f'https://api.example.com/users/{user_id}')
return response.json()
# Нельзя легко мокировать requests!
# ХОРОШО: зависимость как параметр
def get_user_data(user_id, http_client):
response = http_client.get(f'https://api.example.com/users/{user_id}')
return response.json()
# Тестирование
class MockHttpClient:
def get(self, url):
return MockResponse({'id': 1, 'name': 'John'})
result = get_user_data(1, MockHttpClient())
assert result['name'] == 'John'
4. Явные параметры вместо скрытого состояния
# ПЛОХО: метод класса зависит от состояния
class UserProcessor:
def __init__(self):
self.discount = 0.1 # Скрытое состояние
def calculate_price(self, amount):
return amount * (1 - self.discount)
# Сложно тестировать: нужно создавать объект и менять состояние
# ХОРОШО: передать параметр
def calculate_price(amount, discount):
return amount * (1 - discount)
# Легко тестировать
assert calculate_price(100, 0.1) == 90
assert calculate_price(100, 0.2) == 80
5. Вернуть результат вместо I/O
# ПЛОХО: функция выполняет I/O
def process_order(order_id):
order = db.orders.get(order_id)
total = sum(item.price for item in order.items)
# Сохранение прямо в функции
db.orders.update(order_id, {'total': total})
# Отправка сообщения
queue.publish('order_total_calculated', {'order_id': order_id, 'total': total})
return {'success': True}
# Сложно тестировать: нужно мокировать БД и очередь
# ХОРОШО: вернуть результат, пусть вызывающая функция делает I/O
def calculate_order_total(order):
"""Бизнес-логика: только расчёт"""
return sum(item.price for item in order.items)
def process_order(order_id, order_repository, queue_publisher):
"""Оркестрация: управляет I/O"""
order = order_repository.get(order_id)
total = calculate_order_total(order)
order_repository.update(order_id, {'total': total})
queue_publisher.publish('order_total_calculated', {
'order_id': order_id,
'total': total
})
return {'success': True}
# Легко тестировать calculate_order_total
assert calculate_order_total(Order(items=[Item(10), Item(20)])) == 30
6. Избегайте исключений для управления потоком
# ПЛОХО: используем исключения для контроля потока
def find_user(user_id):
try:
return db.users.get(user_id) # Выбрасывает исключение если не найдён
except UserNotFound:
return None
# Сложно тестировать: нужно мокировать выброс исключения
# ХОРОШО: вернуть Optional[User]
def find_user(user_id, repository):
user = repository.find_by_id(user_id)
return user
# Тестирование
class MockRepository:
def find_by_id(self, user_id):
return None
result = find_user(999, MockRepository())
assert result is None
7. Использование типов для самодокументирования
# ПЛОХО: нет типов
def process_data(data, config, logger):
# Что это за объекты? Что они возвращают?
pass
# ХОРОШО: явные типы
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class ProcessConfig:
batch_size: int
timeout: float
retry_count: int
def process_data(
data: List[Dict[str, any]],
config: ProcessConfig,
logger: logging.Logger
) -> List[ProcessedItem]:
"""Обрабатывает данные согласно конфигурации."""
pass
# Легче писать тесты: ясно какие объекты создавать
8. Практический пример тестируемого кода
from typing import Protocol
from dataclasses import dataclass
import pytest
# Определяем интерфейс (Protocol)
class UserRepository(Protocol):
def get_by_id(self, user_id: int) -> 'User':
...
def update(self, user_id: int, data: dict) -> None:
...
class EmailService(Protocol):
def send(self, email: str, subject: str, body: str) -> None:
...
@dataclass
class User:
id: int
name: str
email: str
verified: bool = False
# Бизнес-логика: чистая функция
def generate_verification_email(user: User) -> str:
return f"Hello {user.name}, click here to verify your email"
# Сервис: использует инъекцию зависимостей
class UserVerificationService:
def __init__(
self,
user_repository: UserRepository,
email_service: EmailService
):
self.user_repository = user_repository
self.email_service = email_service
def send_verification_email(self, user_id: int) -> bool:
user = self.user_repository.get_by_id(user_id)
email_body = generate_verification_email(user)
self.email_service.send(user.email, "Verify your email", email_body)
return True
# Тесты
class MockUserRepository:
def __init__(self):
self.users = {1: User(1, "John", "john@example.com")}
def get_by_id(self, user_id: int):
return self.users[user_id]
def update(self, user_id: int, data: dict):
pass
class MockEmailService:
def __init__(self):
self.sent_emails = []
def send(self, email: str, subject: str, body: str):
self.sent_emails.append({
'email': email,
'subject': subject,
'body': body
})
def test_send_verification_email():
# Arrange
user_repo = MockUserRepository()
email_service = MockEmailService()
service = UserVerificationService(user_repo, email_service)
# Act
result = service.send_verification_email(1)
# Assert
assert result is True
assert len(email_service.sent_emails) == 1
assert email_service.sent_emails[0]['email'] == 'john@example.com'
assert 'john' in email_service.sent_emails[0]['body'].lower()
def test_generate_verification_email():
user = User(1, "Jane", "jane@example.com")
email_body = generate_verification_email(user)
assert "jane" in email_body.lower()
assert "verify" in email_body.lower()
Чеклист для тестируемого кода
- Функция чистая: один вход → один выход, без побочных эффектов
- Одна ответственность: функция делает одно
- Зависимости инъецируются: параметры, не глобалы
- Типы явные: Type hints везде
- Результаты возвращаются: не выполняется I/O внутри
- Нет скрытого состояния: всё передаётся параметрами
- Логирование как параметр: не импортируем глобальный logger
- Конфигурация как параметр: не читаем из environment внутри функции
Итог
Тестируемый код — это код со слабыми связями (loose coupling) и сильной когезией (strong cohesion). Он следует принципам SOLID и создаёт простую, понятную архитектуру для себя и для тестов.