← Назад к вопросам

Как напишешь 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 — это инструмент, который это обеспечивает.