Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. unittest.mock: Mock, patch, MagicMock
unittest_mock_basics

unittest.mock: Mock, patch, MagicMock

Основы unittest.mock: создание моков, patch для подмены, проверка вызовов

У вас есть функция send_welcome_email(user) которая вызывает SMTP-сервер. Как протестировать БЕЗ реальной отправки email?

Цель: Научиться создавать моки с unittest.mock и подменять зависимости в тестах.

Вы точно готовы?

Убедитесь, что прошли предыдущий урок:

  • ✅ Понимаете зачем нужны моки
  • ✅ Знаете разницу между mock/stub/fake
  • ✅ Знаете когда использовать мокирование

Если непонятно — вернитесь к Мокирование: зачем и когда.

Проблема: как подменить зависимость?

Код с внешней зависимостью

# src/email_service.py import smtplib def send_email(to, subject, body): """Отправляет email через SMTP""" server = smtplib.SMTP("smtp.gmail.com", 587) server.starttls() server.login("user@gmail.com", "password") server.sendmail("user@gmail.com", to, f"Subject: {subject}\n\n{body}") server.quit() return True

Как протестировать?

# ❌ Плохой тест — реально отправляет email! def test_send_email(): result = send_email("test@example.com", "Hi", "Hello!") assert result == True # Email отправлен на реальный адрес!

Проблемы:

  • ❌ Каждый запуск теста отправляет реальный email
  • ❌ Нужны реальные credentials
  • ❌ Медленно (сетевой запрос)

Что нужно: Подменить smtplib.SMTP на мок-объект.

Решение 1: Mock() — создание мок-объектов

Базовый Mock

from unittest.mock import Mock # Создаём мок-объект mock_server = Mock() # Мок отвечает на ЛЮБОЙ вызов метода mock_server.starttls() # OK mock_server.login("a", "b") # OK mock_server.sendmail() # OK mock_server.any_method() # OK — мок принимает всё! # Проверяем что методы были вызваны mock_server.starttls.assert_called_once() mock_server.login.assert_called_once_with("a", "b")

✅ Mock-объект записывает все вызовы и позволяет их проверять!

return_value: что возвращать

# Мок возвращает другой мок по умолчанию mock = Mock() result = mock.some_method() print(result) # <Mock name='mock.some_method()' id='...'> # Установим конкретное значение mock.some_method.return_value = 42 result = mock.some_method() print(result) # 42

Практический пример:

# src/weather_service.py import requests class WeatherService: def get_temperature(self, city): response = requests.get(f"https://api.weather.com/?city={city}") data = response.json() return data["temperature"] # tests/test_weather.py from unittest.mock import Mock def test_get_temperature(): # Arrange: создаём мок requests mock_response = Mock() mock_response.json.return_value = {"temperature": 15} mock_requests = Mock() mock_requests.get.return_value = mock_response # Act: подменяем requests на мок (пока руками) service = WeatherService() # Как подменить requests? См. @patch ниже!

side_effect: динамическое поведение

# 1. Вызвать исключение mock = Mock() mock.some_method.side_effect = ValueError("Boom!") mock.some_method() # Выбросит ValueError # 2. Вернуть разные значения при каждом вызове mock.some_method.side_effect = [10, 20, 30] print(mock.some_method()) # 10 print(mock.some_method()) # 20 print(mock.some_method()) # 30 # 3. Использовать функцию def custom_logic(arg): return arg * 2 mock.some_method.side_effect = custom_logic print(mock.some_method(5)) # 10 print(mock.some_method(10)) # 20

Практический пример:

# Мокируем API который падает с ошибкой def test_api_timeout(): mock_api = Mock() mock_api.fetch_data.side_effect = TimeoutError("API timeout") service = DataService(api=mock_api) with pytest.raises(TimeoutError): service.get_data()

Решение 2: @patch() — подмена зависимостей

Проблема: как подменить импорт

# src/weather.py import requests # Как подменить этот import? def get_temperature(city): response = requests.get(f"https://api.weather.com/?city={city}") return response.json()["temperature"]

Решение: @patch()

# tests/test_weather.py from unittest.mock import patch @patch("src.weather.requests") # Подменяем requests в модуле src.weather def test_get_temperature(mock_requests): """Тест с подменой requests""" # Arrange: настраиваем мок mock_response = Mock() mock_response.json.return_value = {"temperature": 15} mock_requests.get.return_value = mock_response # Act: вызываем функцию temp = get_temperature("Moscow") # Assert assert temp == 15 mock_requests.get.assert_called_once_with("https://api.weather.com/?city=Moscow")

✅ @patch() автоматически подменяет requests на мок!

Правило: где патчить

Важно: Патчите ТАМ где объект ИСПОЛЬЗУЕТСЯ, не где определён!

# src/email_service.py import smtplib # Импорт здесь def send_email(to, subject, body): server = smtplib.SMTP(...) # Используется здесь # ...

Правильно:

# ✅ Патчим в модуле где используется @patch("src.email_service.smtplib") def test_send_email(mock_smtplib): pass

Неправильно:

# ❌ Патчим в модуле где определён @patch("smtplib") # Не сработает! def test_send_email(mock_smtplib): pass

Множественный патчинг

@patch("src.service.requests") @patch("src.service.open") def test_service(mock_open, mock_requests): # Порядок аргументов ОБРАТНЫЙ порядку декораторов! # mock_open — последний декоратор (второй сверху) # mock_requests — первый декоратор (первый сверху) pass

Проще использовать контекстный менеджер:

def test_service(): with patch("src.service.requests") as mock_requests, \ patch("src.service.open") as mock_open: # Порядок аргументов естественный mock_requests.get.return_value = Mock(json=lambda: {"data": 123}) mock_open.return_value = Mock() # Тест...

Практический пример: EmailService

# src/email_service.py import smtplib def send_email(to, subject, body): """Отправляет email через SMTP""" server = smtplib.SMTP("smtp.gmail.com", 587) server.starttls() server.login("user@gmail.com", "password") server.sendmail("user@gmail.com", to, f"Subject: {subject}\n\n{body}") server.quit() return True # tests/test_email_service.py from unittest.mock import patch, Mock @patch("src.email_service.smtplib.SMTP") def test_send_email(mock_smtp_class): """Тест отправки email БЕЗ реального SMTP""" # Arrange: настраиваем мок SMTP сервера mock_server = Mock() mock_smtp_class.return_value = mock_server # SMTP(...) вернёт наш мок # Act: вызываем функцию result = send_email("test@example.com", "Hello", "Test email") # Assert: проверяем результат assert result == True # Assert: проверяем что SMTP вызван правильно mock_smtp_class.assert_called_once_with("smtp.gmail.com", 587) mock_server.starttls.assert_called_once() mock_server.login.assert_called_once_with("user@gmail.com", "password") mock_server.sendmail.assert_called_once() mock_server.quit.assert_called_once()

✅ Email НЕ отправлен, но мы проверили что код вызывает правильные методы!

Проверка вызовов (verification)

Базовые методы

mock = Mock() mock.method(1, 2, key="value") # Проверка что вызван хотя бы раз mock.method.assert_called() # Проверка что вызван РОВНО один раз mock.method.assert_called_once() # Проверка последнего вызова с конкретными аргументами mock.method.assert_called_with(1, 2, key="value") # Проверка что вызван РОВНО один раз с конкретными аргументами mock.method.assert_called_once_with(1, 2, key="value") # Проверка что НЕ вызван mock.method.assert_not_called()

Продвинутые проверки

mock = Mock() mock.method(1, 2) mock.method(3, 4) # Получить количество вызовов print(mock.method.call_count) # 2 # Получить все вызовы print(mock.method.call_args_list) # [call(1, 2), call(3, 4)] # Проверить любой вызов (ANY) from unittest.mock import ANY mock.method.assert_called_with(1, ANY) # Второй аргумент любой

Практический пример

@patch("src.logger.Logger") def test_service_logs_errors(mock_logger_class): """Проверяем что сервис логирует ошибки""" mock_logger = Mock() mock_logger_class.return_value = mock_logger service = Service() service.process_invalid_data({"bad": "data"}) # Проверяем что logger.error был вызван mock_logger.error.assert_called_once() # Проверяем сообщение об ошибке call_args = mock_logger.error.call_args message = call_args[0][0] # Первый позиционный аргумент assert "Invalid data" in message

MagicMock: для магических методов (bonus)

Проблема с обычным Mock

mock = Mock() # Магические методы не работают! len(mock) # TypeError mock[0] # TypeError

Решение: MagicMock

from unittest.mock import MagicMock mock = MagicMock() # Магические методы работают! mock.__len__.return_value = 5 print(len(mock)) # 5 mock.__getitem__.return_value = "value" print(mock[0]) # "value"

Практический пример:

@patch("src.service.open", MagicMock()) def test_read_file(mock_open): """Тест чтения файла""" # Настраиваем мок файла с контекстным менеджером mock_file = MagicMock() mock_file.__enter__.return_value.read.return_value = "file content" mock_open.return_value = mock_file # Код использует: with open(...) as f: content = f.read() content = read_config_file("config.txt") assert content == "file content"

Когда использовать:

  • Mock() — в 95% случаев (достаточно)
  • MagicMock() — когда нужны __len__, __getitem__, __enter__ и т.д.

Практический пример: полный тест

# src/order_service.py import requests from datetime import datetime class OrderService: def __init__(self, api_url): self.api_url = api_url def create_order(self, user_id, items): """Создаёт заказ через API""" # 1. Получаем данные пользователя response = requests.get(f"{self.api_url}/users/{user_id}") user = response.json() # 2. Создаём заказ order_data = { "user": user, "items": items, "created_at": datetime.now().isoformat() } response = requests.post(f"{self.api_url}/orders", json=order_data) return response.json() # tests/test_order_service.py from unittest.mock import patch, Mock import pytest @patch("src.order_service.datetime") @patch("src.order_service.requests") def test_create_order(mock_requests, mock_datetime): """Тест создания заказа БЕЗ реального API""" # Arrange: мокируем requests.get (получение пользователя) mock_user_response = Mock() mock_user_response.json.return_value = { "id": 1, "name": "Alice", "email": "alice@example.com" } # Arrange: мокируем requests.post (создание заказа) mock_order_response = Mock() mock_order_response.json.return_value = { "id": 101, "status": "created" } # requests.get/post возвращают разные моки mock_requests.get.return_value = mock_user_response mock_requests.post.return_value = mock_order_response # Arrange: мокируем datetime.now() mock_datetime.now.return_value.isoformat.return_value = "2025-01-15T10:00:00" # Act: создаём заказ service = OrderService(api_url="https://api.example.com") order = service.create_order(user_id=1, items=["item1", "item2"]) # Assert: проверяем результат assert order["id"] == 101 assert order["status"] == "created" # Assert: проверяем что API вызван правильно mock_requests.get.assert_called_once_with("https://api.example.com/users/1") mock_requests.post.assert_called_once() # Assert: проверяем данные отправленные в POST call_args = mock_requests.post.call_args sent_data = call_args[1]["json"] # Именованный аргумент 'json' assert sent_data["user"]["name"] == "Alice" assert sent_data["items"] == ["item1", "item2"] assert sent_data["created_at"] == "2025-01-15T10:00:00"

✅ Тест работает мгновенно БЕЗ реального API!

Что вы изучили

  • Mock() — создание мок-объектов
  • return_value — что возвращать при вызове
  • side_effect — динамическое поведение (исключения, последовательности)
  • @patch() — подмена зависимостей в тестах
  • Verification — assert_called_once_with(), call_count, call_args
  • MagicMock — для магических методов (len, getitem)

Проверьте свои знания

Вопросы ещё не добавлены

Вопросы для этой подтемы ещё не добавлены.

Далее: Мокирование HTTP: requests-mock