← Назад к вопросам
Как напишешь Unit тест для кода, который взаимодействует с почтой?
2.0 Middle🔥 121 комментариев
#Python Core#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Unit тесты для кода с отправкой почты
Для тестирования кода с почтой нужно использовать mocking — это ключевой паттерн в юнит-тестировании.
1. Использование unittest.mock
Основной подход — заменить реальное отправление письма на mock объект:
import unittest
from unittest.mock import patch, MagicMock
from datetime import datetime
class EmailService:
def __init__(self, smtp_server):
self.smtp = smtp_server
def send_welcome_email(self, user_email, username):
"""Отправить приветственное письмо"""
subject = f"Добро пожаловать, {username}!"
body = f"Привет, {username}! Спасибо, что присоединился к нам."
self.smtp.send(to=user_email, subject=subject, body=body)
return True
class TestEmailService(unittest.TestCase):
@patch('email_service.SMTP') # Заменить реальный SMTP на mock
def test_send_welcome_email(self, mock_smtp):
# Организация (Arrange)
service = EmailService(mock_smtp)
user_email = "john@example.com"
username = "john_doe"
# Действие (Act)
result = service.send_welcome_email(user_email, username)
# Проверка (Assert)
self.assertTrue(result)
# Проверить, что метод send был вызван с правильными параметрами
mock_smtp.send.assert_called_once_with(
to=user_email,
subject="Добро пожаловать, john_doe!",
body="Привет, john_doe! Спасибо, что присоединился к нам."
)
2. Более практичный пример с pytest и pytest-mock
import pytest
from datetime import datetime
from decimal import Decimal
class OrderNotificationService:
def __init__(self, email_backend):
self.email_backend = email_backend
def notify_order_placed(self, order_id, customer_email):
"""Уведомить о размещённом заказе"""
message = f"Ваш заказ #{order_id} принят в обработку."
return self.email_backend.send(
to=customer_email,
subject="Подтверждение заказа",
body=message,
priority="high"
)
@pytest.fixture
def email_service(mocker):
"""Fixture с mock email backend"""
mock_backend = mocker.MagicMock()
mock_backend.send.return_value = {"status": "sent", "message_id": "msg_123"}
return OrderNotificationService(mock_backend)
def test_notify_order_placed(email_service, mocker):
"""Тест отправки уведомления о заказе"""
# Организация
order_id = "ORDER-12345"
customer_email = "customer@example.com"
# Действие
result = email_service.notify_order_placed(order_id, customer_email)
# Проверка
assert result["status"] == "sent"
assert result["message_id"] == "msg_123"
# Проверить вызов
email_service.email_backend.send.assert_called_once_with(
to=customer_email,
subject="Подтверждение заказа",
body="Ваш заказ #ORDER-12345 принят в обработку.",
priority="high"
)
3. Тестирование ошибок при отправке
import pytest
from unittest.mock import patch
class SMTPError(Exception):
pass
class EmailService:
def __init__(self, smtp):
self.smtp = smtp
def send_email(self, to, subject, body):
try:
return self.smtp.send(to=to, subject=subject, body=body)
except SMTPError as e:
# Логировать ошибку и вернуть False
print(f"Ошибка отправки письма: {e}")
return False
class TestEmailServiceErrors:
@patch('email_service.SMTP')
def test_send_email_smtp_error(self, mock_smtp):
"""Тест обработки SMTP ошибки"""
# Сконфигурировать mock на выброс исключения
mock_smtp.send.side_effect = SMTPError("Connection timeout")
service = EmailService(mock_smtp)
result = service.send_email("user@example.com", "Test", "Body")
# Проверить, что ошибка обработана
assert result is False
mock_smtp.send.assert_called_once()
4. Проверка содержимого письма
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import json
class RichEmailService:
def __init__(self, smtp):
self.smtp = smtp
def send_html_email(self, to, subject, html_body, attachments=None):
msg = MIMEMultipart()
msg['From'] = 'noreply@example.com'
msg['To'] = to
msg['Subject'] = subject
msg.attach(MIMEText(html_body, 'html'))
for attachment in (attachments or []):
msg.attach(attachment)
return self.smtp.send_message(msg)
@pytest.fixture
def email_service(mocker):
mock_smtp = mocker.MagicMock()
return RichEmailService(mock_smtp)
def test_send_html_email_with_attachments(email_service):
"""Тест отправки HTML письма с вложениями"""
html_body = "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
attachments = [MIMEText("attachment content")]
email_service.send_html_email(
to="user@example.com",
subject="Welcome",
html_body=html_body,
attachments=attachments
)
# Проверить, что send_message был вызван один раз
assert email_service.smtp.send_message.call_count == 1
# Получить переданное письмо
call_args = email_service.smtp.send_message.call_args
sent_message = call_args[0][0]
# Проверить заголовки
assert sent_message['Subject'] == 'Welcome'
assert sent_message['To'] == 'user@example.com'
5. Параметризованные тесты для разных сценариев
import pytest
class NotificationService:
def __init__(self, email_backend):
self.email_backend = email_backend
def send_notification(self, user_email, notification_type, data):
templates = {
'password_reset': 'Reset your password here: {link}',
'order_confirmed': 'Your order #{order_id} has been confirmed',
'payment_received': 'Payment of {amount} received'
}
subject_map = {
'password_reset': 'Password Reset Request',
'order_confirmed': 'Order Confirmed',
'payment_received': 'Payment Received'
}
body = templates[notification_type].format(**data)
subject = subject_map[notification_type]
return self.email_backend.send(to=user_email, subject=subject, body=body)
@pytest.mark.parametrize('notification_type,data,expected_subject', [
('password_reset', {'link': 'https://example.com/reset'}, 'Password Reset Request'),
('order_confirmed', {'order_id': '12345'}, 'Order Confirmed'),
('payment_received', {'amount': '100 USD'}, 'Payment Received'),
])
def test_send_notification_types(mocker, notification_type, data, expected_subject):
"""Тест отправки разных типов уведомлений"""
mock_email = mocker.MagicMock()
service = NotificationService(mock_email)
service.send_notification('user@example.com', notification_type, data)
# Проверить правильный subject
call_args = mock_email.send.call_args
assert call_args[1]['subject'] == expected_subject
6. Спай-объекты для отслеживания вызовов
from unittest.mock import Mock, call
class BatchEmailService:
def __init__(self, smtp):
self.smtp = smtp
def send_bulk(self, recipients, subject, body):
for email in recipients:
self.smtp.send(to=email, subject=subject, body=body)
def test_send_bulk_emails():
"""Тест отправки пакета писем"""
mock_smtp = Mock()
service = BatchEmailService(mock_smtp)
recipients = ['user1@example.com', 'user2@example.com', 'user3@example.com']
service.send_bulk(recipients, 'Newsletter', 'Latest news')
# Проверить, что send был вызван 3 раза
assert mock_smtp.send.call_count == 3
# Проверить все вызовы
expected_calls = [
call(to='user1@example.com', subject='Newsletter', body='Latest news'),
call(to='user2@example.com', subject='Newsletter', body='Latest news'),
call(to='user3@example.com', subject='Newsletter', body='Latest news'),
]
mock_smtp.send.assert_has_calls(expected_calls)
Ключевые моменты
- Mock объекты — заменяют реальные зависимости
- patch — заменить импорт во время теста
- MagicMock — автоматически создаёт методы при обращении
- side_effect — вызвать исключение или вернуть разные значения
- assert_called_once_with() — проверить точный вызов
- Не отправляй реальные письма — всегда используй mock
- Тестируй различные сценарии — успех, ошибка, таймаут
Юнит-тесты должны быть быстрыми, изолированными и предсказуемыми. Mocking — это инструмент, который это обеспечивает.