freezegun для тестов с датами, patch для file I/O. Тесты без побочных эффектов
У вас есть функция calculate_age(birthdate) которая использует datetime.now(). Тест работает сегодня, но сломается через год. Как сделать тест стабильным?
Цель: Научиться мокировать время и файловый ввод-вывод.
Убедитесь, что умеете:
# Мокировать HTTP с requests-mock
import requests_mock
def test_api(mock_requests):
mock_requests.get("https://api.example.com/data", json={"value": 123})Если requests-mock непонятен — вернитесь к Мокирование HTTP.
# src/user.py
from datetime import datetime
class User:
def __init__(self, name, birthdate):
self.name = name
self.birthdate = birthdate # "2000-01-15"
def get_age(self):
"""Возвращает возраст пользователя"""
today = datetime.now().date()
birth = datetime.fromisoformat(self.birthdate).date()
age = today.year - birth.year
# Корректируем если день рождения ещё не наступил
if (today.month, today.day) < (birth.month, birth.day):
age -= 1
return age
def has_birthday_today(self):
"""Проверяет день рождения сегодня"""
today = datetime.now().date()
birth = datetime.fromisoformat(self.birthdate).date()
return (today.month, today.day) == (birth.month, birth.day)# ❌ ПЛОХО — тест зависит от даты запуска
def test_user_age():
user = User("Alice", "2000-01-15")
# Если сегодня 2025-01-16 — возраст 25
# Если сегодня 2025-01-14 — возраст 24
# Тест нестабильный!
age = user.get_age()
assert age == 25 # Сломается через год или до дня рождения!
def test_birthday_today():
user = User("Alice", "2000-01-15")
# ❌ Работает только 15 января!
assert user.has_birthday_today() == TrueПроблемы:
pip install freezegunfrom freezegun import freeze_time
from datetime import datetime
@freeze_time("2025-01-15")
def test_frozen_time():
"""Время 'заморожено' на 15 января 2025"""
now = datetime.now()
print(now) # 2025-01-15 00:00:00
# Время не меняется!
import time
time.sleep(1)
print(datetime.now()) # Всё ещё 2025-01-15 00:00:00✅ Тест всегда работает с одной и той же датой!
from freezegun import freeze_time
@freeze_time("2025-01-16") # Сегодня 16 января 2025
def test_user_age():
"""Возраст пользователя родившегося 15 января 2000"""
user = User("Alice", "2000-01-15")
age = user.get_age()
assert age == 25 # 16 января 2025 — уже 25 лет
@freeze_time("2025-01-14") # Сегодня 14 января 2025 (ДО дня рождения)
def test_user_age_before_birthday():
"""Возраст до дня рождения"""
user = User("Alice", "2000-01-15")
age = user.get_age()
assert age == 24 # День рождения ещё не наступил — 24 года✅ Тесты стабильны и проверяют разные сценарии!
@freeze_time("2025-01-15")
def test_birthday_today():
"""Проверка дня рождения"""
user = User("Alice", "2000-01-15")
assert user.has_birthday_today() == True
@freeze_time("2025-01-14")
def test_not_birthday():
"""Не день рождения"""
user = User("Alice", "2000-01-15")
assert user.has_birthday_today() == Falsefrom freezegun import freeze_time
def test_time_travel():
"""Можно 'путешествовать' во времени внутри теста"""
with freeze_time("2025-01-01"):
assert datetime.now().day == 1
with freeze_time("2025-12-31"):
assert datetime.now().day == 31
# Вне блока — реальное время
print(datetime.now()) # Реальная дата@freeze_time("2025-01-15 10:30:00")
def test_specific_time():
"""Заморозка конкретного времени"""
now = datetime.now()
assert now.hour == 10
assert now.minute == 30
assert now.second == 0# src/session_manager.py
from datetime import datetime, timedelta
class SessionManager:
def __init__(self, timeout_minutes=30):
self.timeout_minutes = timeout_minutes
self.sessions = {}
def create_session(self, user_id):
"""Создаёт сессию"""
session_id = f"session_{user_id}"
self.sessions[session_id] = {
"user_id": user_id,
"created_at": datetime.now(),
"expires_at": datetime.now() + timedelta(minutes=self.timeout_minutes)
}
return session_id
def is_valid(self, session_id):
"""Проверяет валидность сессии"""
if session_id not in self.sessions:
return False
session = self.sessions[session_id]
return datetime.now() < session["expires_at"]
# tests/test_session_manager.py
from freezegun import freeze_time
@freeze_time("2025-01-15 10:00:00")
def test_create_session():
"""Создание сессии"""
manager = SessionManager(timeout_minutes=30)
session_id = manager.create_session(user_id=1)
session = manager.sessions[session_id]
assert session["created_at"] == datetime(2025, 1, 15, 10, 0, 0)
assert session["expires_at"] == datetime(2025, 1, 15, 10, 30, 0)
@freeze_time("2025-01-15 10:00:00")
def test_session_valid():
"""Сессия валидна сразу после создания"""
manager = SessionManager(timeout_minutes=30)
session_id = manager.create_session(user_id=1)
assert manager.is_valid(session_id) == True
@freeze_time("2025-01-15 10:00:00")
def test_session_expires():
"""Сессия истекает через timeout"""
manager = SessionManager(timeout_minutes=30)
session_id = manager.create_session(user_id=1)
# Перемещаемся на 31 минуту вперёд
with freeze_time("2025-01-15 10:31:00"):
assert manager.is_valid(session_id) == False
@freeze_time("2025-01-15 10:00:00")
def test_session_still_valid():
"""Сессия валидна за 1 минуту до истечения"""
manager = SessionManager(timeout_minutes=30)
session_id = manager.create_session(user_id=1)
# 29 минут спустя — ещё валидна
with freeze_time("2025-01-15 10:29:00"):
assert manager.is_valid(session_id) == True✅ Можем тестировать таймауты БЕЗ ожидания!
# src/config_reader.py
def read_config(filename):
"""Читает конфигурацию из файла"""
with open(filename, "r") as f:
content = f.read()
return content
def save_report(filename, data):
"""Сохраняет отчёт в файл"""
with open(filename, "w") as f:
f.write(data)
return Trueimport os
def test_read_config():
# ❌ Создаём реальный файл
with open("test_config.txt", "w") as f:
f.write("config_value=123")
content = read_config("test_config.txt")
assert "config_value" in content
# Cleanup
os.remove("test_config.txt")Проблемы:
from unittest.mock import patch, mock_open
@patch("builtins.open", mock_open(read_data="config_value=123"))
def test_read_config():
"""Тест чтения файла БЕЗ реального файла"""
content = read_config("any_filename.txt")
assert "config_value" in content✅ Файл НЕ создан, всё работает в памяти!
@patch("builtins.open", mock_open())
def test_save_report(mock_file):
"""Тест записи файла"""
# Act
result = save_report("report.txt", "Test data")
# Assert
assert result == True
# Проверяем что open был вызван правильно
mock_file.assert_called_once_with("report.txt", "w")
# Проверяем что write был вызван с правильными данными
handle = mock_file()
handle.write.assert_called_once_with("Test data")# src/file_processor.py
def process_file(input_file, output_file):
"""Читает файл, обрабатывает и записывает результат"""
with open(input_file, "r") as f:
content = f.read()
processed = content.upper() # Простая обработка
with open(output_file, "w") as f:
f.write(processed)
return True
# tests/test_file_processor.py
from unittest.mock import patch, mock_open, call
@patch("builtins.open")
def test_process_file(mock_file):
"""Тест обработки файла"""
# Мокируем чтение (input)
mock_file.return_value.__enter__.return_value.read.return_value = "hello world"
# Act
result = process_file("input.txt", "output.txt")
# Assert
assert result == True
# Проверяем что open вызван дважды (чтение + запись)
assert mock_file.call_count == 2
# Проверяем первый вызов (чтение)
assert mock_file.call_args_list[0] == call("input.txt", "r")
# Проверяем второй вызов (запись)
assert mock_file.call_args_list[1] == call("output.txt", "w")
# Проверяем что записано
handle = mock_file()
handle.write.assert_called_once_with("HELLO WORLD")# src/log_manager.py
from datetime import datetime
class LogManager:
def __init__(self, log_file):
self.log_file = log_file
def log(self, level, message):
"""Записывает лог в файл"""
timestamp = datetime.now().isoformat()
log_entry = f"[{timestamp}] {level}: {message}\n"
with open(self.log_file, "a") as f:
f.write(log_entry)
def get_logs(self):
"""Читает все логи"""
try:
with open(self.log_file, "r") as f:
return f.read()
except FileNotFoundError:
return ""
# tests/test_log_manager.py
from unittest.mock import patch, mock_open
from freezegun import freeze_time
@freeze_time("2025-01-15 10:30:00")
@patch("builtins.open", mock_open())
def test_log_entry(mock_file):
"""Тест записи лога"""
logger = LogManager("app.log")
logger.log("INFO", "Application started")
# Проверяем что файл открыт в режиме append
mock_file.assert_called_once_with("app.log", "a")
# Проверяем содержимое
handle = mock_file()
handle.write.assert_called_once_with(
"[2025-01-15T10:30:00] INFO: Application started\n"
)
@patch("builtins.open", mock_open(read_data="[2025-01-15T10:00:00] INFO: Test log\n"))
def test_get_logs(mock_file):
"""Тест чтения логов"""
logger = LogManager("app.log")
logs = logger.get_logs()
assert "Test log" in logs
mock_file.assert_called_once_with("app.log", "r")
@patch("builtins.open", side_effect=FileNotFoundError)
def test_get_logs_file_not_exists(mock_file):
"""Тест когда файла нет"""
logger = LogManager("app.log")
logs = logger.get_logs()
assert logs == ""# src/backup_manager.py
from datetime import datetime
import json
class BackupManager:
def create_backup(self, data):
"""Создаёт резервную копию с timestamp"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"backup_{timestamp}.json"
with open(filename, "w") as f:
json.dump(data, f)
return filename
# tests/test_backup_manager.py
from freezegun import freeze_time
from unittest.mock import patch, mock_open
@freeze_time("2025-01-15 10:30:00")
@patch("builtins.open", mock_open())
def test_create_backup(mock_file):
"""Тест создания бэкапа"""
manager = BackupManager()
data = {"users": [1, 2, 3]}
filename = manager.create_backup(data)
# Проверяем имя файла
assert filename == "backup_20250115_103000.json"
# Проверяем что файл создан
mock_file.assert_called_once_with("backup_20250115_103000.json", "w")
# Проверяем что данные записаны
handle = mock_file()
written_data = handle.write.call_args[0][0]
assert "users" in written_data✅ Тестируем время И файлы БЕЗ побочных эффектов!
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Coverage: контроль качества тестов