Python · Тестирование с pytest · Средний

Мокирование с unittest.mock

5 задач

Изолируйте тестируемый код, заменяя зависимости на управляемые фейки.

Мокирование: unittest.mock и pytest-mock

#
## Проблема, которую решает мокирование Начнём с простой функции, получающей текущую температуру для города: ```python # weather.py import requests def get_temperature(city): resp = requests.get(f'https://api.weather.example.com/temp/{city}') return resp.json()['temperature'] ``` Напишем тест: ```python def test_get_temperature(): result = get_temperature('Kyiv') assert result == 22 ``` У этого теста серьёзные проблемы: - **Падает при отсутствии интернета** -- даже если ваш код абсолютно верен - **Падает при изменении данных API** -- `{'temperature': 22}` сегодня, `{'temp': 22}` завтра - **Нельзя протестировать путь ошибки** -- как заставить API вернуть ошибку 500 по требованию? - **Медленно** -- реальный HTTP-вызов при каждом запуске тестов добавляет секунды - **Непредсказуемо** -- температура меняется; нельзя написать `assert result == 22` Тест проверяет не *ваш* код -- он тестирует API погоды, сеть и интернет-соединение одновременно. --- ## Ключевая идея: контролируемый заменитель **Мок** -- это объект, имитирующий реальную зависимость, но полностью находящийся под вашим контролем. Вместо того чтобы ваш код обращался к реальному API, он вызывает подставной объект, который вы заранее запрограммировали возвращать именно те данные, которые нужны. Представьте это как контролируемый эксперимент. В химии не проверяют действие таблетки, давая её случайным людям в разных условиях -- контролируют каждую переменную, чтобы измерить ровно одну вещь. Мок делает то же самое: убирает все переменные внешней зависимости, и тест измеряет только ваш код. ``` Реальный тест (плохо): ваш код -> requests -> интернет -> реальный сервер -> реальные данные -> ваш код ^ любой из них может упасть непредсказуемо Тест с моком (хорошо): ваш код -> мок -> ваши данные ^ всегда быстро, всегда возвращает то, что вы сказали, всегда доступно ``` --- ## Шаг 1 -- MagicMock: ваш первый фиктивный объект `MagicMock` -- это строительный блок. Объект, который: - Принимает **любой вызов метода**, не выбрасывая `AttributeError` - По умолчанию возвращает ещё один `MagicMock` из каждого вызова - **Записывает каждое взаимодействие**, чтобы потом можно было проверить, что происходило ```python from unittest.mock import MagicMock mock = MagicMock() # Обращение к любому атрибуту -- он просто создаётся автоматически: print(mock.name) # <MagicMock name='mock.name' id='...'> print(mock.does_not_exist) # нет AttributeError -- создаётся автоматически # Вызов любого метода -- то же самое: result = mock.calculate(10, 5) # не выбрасывает; возвращает ещё один MagicMock ``` Это поведение "создать всё автоматически" -- вот почему он называется *Magic*Mock. Никогда не нужно заранее объявлять, какие атрибуты или методы существуют. ### Установка возвращаемых значений Используйте `return_value`, чтобы контролировать, что метод возвращает при вызове: ```python mock = MagicMock() mock.calculate.return_value = 42 result = mock.calculate(10, 5) print(result) # 42 ``` ### Инспекция произошедшего (запись) После вызова методов на моке можно узнать точно, что произошло: ```python mock = MagicMock() mock.send('hello', recipient='alice') # Был ли вызов вообще? mock.send.assert_called() # проходит # Был ли вызов ровно один раз? mock.send.assert_called_once() # проходит # Был ли вызов с этими точными аргументами? mock.send.assert_called_once_with('hello', recipient='alice') # проходит # Что если проверить неверные аргументы? mock.send.assert_called_once_with('wrong') # AssertionError! # Сколько раз всего? print(mock.send.call_count) # 1 ``` Запись -- вот что делает моки полезными как *тест-шпионы*: вы не только контролируете, что возвращает зависимость, но и можете проверить, что ваш код вызвал её правильно. --- ## Шаг 2 -- Почему одного MagicMock недостаточно Можно создать идеальный фиктивный объект, но есть проблема: ваш код уже импортировал `requests` и держит ссылку на него. Создание нового `MagicMock()` в тесте никак не влияет на то, что вызывает ваша функция. ```python # Это НЕ работает: def test_get_temperature_broken(): fake_requests = MagicMock() fake_requests.get.return_value.json.return_value = {'temperature': 22} result = get_temperature('Kyiv') # всё равно вызывает НАСТОЯЩИЙ requests.get! assert result == 22 ``` Почему? Потому что `weather.py` выполнил `import requests` при первой загрузке. Имя `requests` внутри `weather.py` привязано к реальному модулю `requests`. Переменная `fake_requests` в тесте -- просто локальная переменная, не имеющая никакого отношения к тому, что использует `weather.py`. Чтобы `get_temperature` использовала ваш фиктивный объект, нужно заменить имя `requests` *внутри* модуля `weather`. Именно это делает `patch`. --- ## Шаг 3 -- patch: внедрение мока в нужное место `patch` временно заменяет имя в пространстве имён модуля на `MagicMock` (или любой объект по выбору), выполняет тест, затем восстанавливает оригинал: ```python from unittest.mock import patch with patch('weather.requests') as mock_requests: # Внутри этого блока weather.requests И ЕСТЬ мок mock_requests.get.return_value.json.return_value = {'temperature': 22} result = get_temperature('Kyiv') assert result == 22 # После блока weather.requests снова является реальным модулем requests ``` Строка `'weather.requests'` -- это адрес заменяемого имени: *"зайди в модуль `weather` и замени его атрибут `requests`"*. --- ## Построение полного мок-теста, строка за строкой Полный тест с пояснениями: ```python from unittest.mock import patch import weather @patch('weather.requests.get') # (1) def test_get_temperature(mock_get): # (2) mock_get.return_value.json.return_value = {'temperature': 22} # (3) result = weather.get_temperature('Kyiv') # (4) assert result == 22 # (5) mock_get.assert_called_once_with( # (6) 'https://api.weather.example.com/temp/Kyiv' ) ``` **(1)** `@patch('weather.requests.get')` -- заменяет только `requests.get` внутри модуля `weather` (патчим конкретную функцию, а не весь модуль). **(2)** `def test_get_temperature(mock_get)` -- pytest получает `MagicMock`, заменивший `requests.get`, как дополнительный аргумент. **(3)** `mock_get.return_value.json.return_value = {'temperature': 22}` -- самая каверзная строка. Разберём её: - `mock_get(...)` -- наш фиктивный `requests.get(...)` -- возвращает `mock_get.return_value` (фиктивный объект ответа) - На этом объекте ответа вызывается `.json()` -- значит, `mock_get.return_value.json` -- это фиктивный метод `.json` - `.return_value = {'temperature': 22}` делает так, что `.json()` возвращает наш словарь Иными словами: `mock_get.return_value` = фиктивный ответ, `.json.return_value` = то, что возвращает `.json()`. **(4)** `weather.get_temperature('Kyiv')` -- вызывает реальную функцию. Она вызовет `requests.get(...)`, который теперь является нашим моком, получит фиктивный ответ, вызовет на нём `.json()` и вернёт `22`. **(5)** Проверяем, что результат соответствует запрограммированному. **(6)** Проверяем, что код действительно вызвал API с правильным URL -- это верифицирует, что функция правильно построила URL, а не только то, что она вернула верное значение. ### Визуализация цепочки мока ``` weather.py вызывает: requests.get(url) -> resp -> resp.json() -> {'temperature': 22} Мок-эквивалент: mock_get(url) | возвращает mock_get.return_value (фиктивный объект ответа) | вызывается .json() mock_get.return_value.json() | возвращает mock_get.return_value.json.return_value <- установите сюда свой словарь ``` Каждая `.` в цепочке добавляет один уровень `return_value`. Как только вы видите этот паттерн, цепочки становятся предсказуемыми. --- ## Что можно тестировать с моками, чего нельзя без них ```python import requests # Основной сценарий -- нормальный ответ: mock_get.return_value.json.return_value = {'temperature': 22} # Ошибка сервера -- API возвращает HTTP 500: mock_get.return_value.raise_for_status.side_effect = requests.HTTPError('500 Server Error') # Сбой сети -- нет соединения: mock_get.side_effect = ConnectionError('network unreachable') # Таймаут -- API слишком медленный: mock_get.side_effect = requests.Timeout('read timeout') ``` Тестировать эти сценарии с реальным API практически невозможно. С моками -- по одной строке на каждый. --- ## Часть 1 -- unittest.mock (стандартная библиотека) Несмотря на название, `unittest.mock` не привязан к фреймворку `unittest`. Это **универсальный модуль мокирования Python** (в стандартной библиотеке с Python 3.3), который одинаково используется pytest, unittest и любыми другими инструментами. Установка не требуется: ```python from unittest.mock import MagicMock, patch ``` ### MagicMock -- записывающий фиктивный объект `MagicMock` автоматически создаёт любой атрибут или метод, к которому вы обращаетесь. Каждый вызов возвращает ещё один `MagicMock`, если не указано иное -- `AttributeError` не возникает никогда: ```python from unittest.mock import MagicMock mock = MagicMock() mock.method.return_value = 42 result = mock.method('any', 'args') # -> 42 mock.method.assert_called_once_with('any', 'args') # проходит print(mock.method.call_count) # 1 ``` ### patch -- замена имени в пространстве имён модуля `patch` временно подменяет имя в пространстве имён модуля на время теста, затем автоматически восстанавливает оригинал. **Форма декоратора** -- мок передаётся как дополнительный аргумент: ```python from unittest.mock import patch @patch('mymodule.requests.get') def test_fetch(mock_get): mock_get.return_value.json.return_value = {'price': 150} result = mymodule.fetch_price('AAPL') assert result == 150 ``` **Форма контекстного менеджера** -- полезна, когда нужно патчить только часть теста: ```python def test_fetch(): with patch('mymodule.requests.get') as mock_get: mock_get.return_value.json.return_value = {'price': 150} result = mymodule.fetch_price('AAPL') assert result == 150 ``` ### Правило пути патча: где *используется*, а не где *определено* Это самый распространённый источник путаницы. Патчите имя **так, как ваш модуль его видит**: ```python # Если mymodule.py делает: import requests @patch('mymodule.requests.get') # правильно # Если mymodule.py делает: from requests import get @patch('mymodule.get') # правильно -- 'get' теперь собственное имя в mymodule @patch('requests.get') # неправильно -- mymodule.get уже держит отдельную ссылку ``` **Почему это важно:** когда Python выполняет `import requests` в вашем модуле, он создаёт имя `requests` в пространстве имён этого модуля, указывающее на объект модуля. Патчинг `mymodule.requests.get` заменяет атрибут `get` в этом пространстве имён. Если патчить `requests.get` напрямую, вы изменяете оригинальный модуль -- но если `mymodule` использовал `from requests import get`, у него уже есть собственная копия ссылки на функцию, и ваш патч её не затронет. Мысленная модель: путь патча -- это почтовый адрес. `'weather.requests.get'` означает *"зайти в модуль weather (здание), найти объект requests (этаж), заменить get (дверь)"*. Нужно указать адрес, по которому функция *живёт* в вашем коде, а не откуда она была изначально доставлена. ### return_value и side_effect ```python mock.return_value = 42 # возвращает 42 при каждом вызове mock.side_effect = ConnectionError # выбрасывает этот класс исключения при вызове mock.side_effect = [1, 2, 3] # возвращает элементы по порядку, по одному за вызов mock.side_effect = lambda x: x * 2 # вызывает эту функцию с теми же аргументами ``` `side_effect` переопределяет `return_value` при установке. ### Распространённые ошибки начинающих **Ошибка 1: патчинг неправильного места** ```python # Код делает: import requests @patch('requests.get') # неправильно -- патчит источник, а не место использования @patch('mymodule.requests.get') # правильно ``` **Ошибка 2: забытое цепочечное return_value** ```python # неправильно -- устанавливает то, что возвращает сам mock_get, а не то, что возвращает .json() mock_get.return_value = {'temperature': 22} # правильно -- resp.json() должен возвращать словарь, а не сам resp mock_get.return_value.json.return_value = {'temperature': 22} ``` **Ошибка 3: утверждение до вызова** ```python @patch('mymodule.func') def test_something(mock_func): mock_func.assert_called() # падает -- ничего ещё не вызывало его result = mymodule.do_something() mock_func.assert_called() # проходит -- сначала вызов, потом утверждение ``` **Ошибка 4: отсутствие утверждения вообще** ```python @patch('mymodule.requests.get') def test_fetch(mock_get): mock_get.return_value.json.return_value = {'data': 1} result = mymodule.fetch() assert result == 1 # Отсутствует: проверка, что mock_get вызван с правильным URL # Функция могла хардкодить неверный URL, и тест всё равно проходит ``` --- ## Часть 2 -- pytest-mock (обёртка в стиле pytest) `pytest-mock` -- тонкий плагин, оборачивающий `unittest.mock` в pytest-фикстуру под названием `mocker`. Установить один раз: ```bash pip install pytest-mock ``` Фикстура `mocker` внедряется как любая другая pytest-фикстура -- **декоратор не нужен**: ```python def test_fetch(mocker): mock_get = mocker.patch('mymodule.requests.get') mock_get.return_value.json.return_value = {'price': 150} result = mymodule.fetch_price('AAPL') assert result == 150 ``` `mocker.patch` возвращает тот же объект `MagicMock`, поэтому все методы утверждений идентичны. ### Что предоставляет mocker ```python mocker.patch('mymodule.func') # замена имени (возвращает MagicMock) mocker.patch.object(instance, 'method') # замена метода на конкретном экземпляре mocker.MagicMock() # создание автономного мока вручную mocker.patch('mymodule.func', return_value=42) # сокращение: установка return_value сразу ``` Все моки, созданные через `mocker`, автоматически сбрасываются и останавливаются после завершения теста -- ручная очистка не нужна. ### Где mocker явно выигрывает: множественные патчи При `@patch` стекирование декораторов переворачивает порядок аргументов -- известная ловушка: ```python @patch('mymodule.requests.get') @patch('mymodule.time.sleep') @patch('mymodule.logger.warning') def test_retry(mock_warn, mock_sleep, mock_get): # перевёрнуто! нижний декоратор -> первый аргумент ... ``` С `mocker` каждый патч -- именованная локальная переменная в естественном порядке: ```python def test_retry(mocker): mock_get = mocker.patch('mymodule.requests.get') mock_sleep = mocker.patch('mymodule.time.sleep') mock_warn = mocker.patch('mymodule.logger.warning') # нет путаницы с порядком, каждый мок имеет осмысленное имя ``` ### mocker в фикстурах -- естественное сочетание Поскольку `mocker` -- это фикстура, её можно передавать напрямую в другие фикстуры: ```python @pytest.fixture def patched_client(mocker): mocker.patch('mymodule.requests.get', return_value=mocker.MagicMock( status_code=200, json=lambda: {'results': []}, )) return mymodule.ApiClient() ``` С сырым `@patch` то же самое внутри фикстуры требует неудобного контекстного менеджера. --- ## Выбор между ними | | `unittest.mock` напрямую | `pytest-mock` | |---|---|---| | **Установка** | встроен -- не нужна | `pip install pytest-mock` | | **Стиль** | декоратор `@patch` или `with patch()` | параметр-фикстура `mocker` | | **Множественные моки** | стекированные декораторы, перевёрнутый порядок | отдельные вызовы, именованные переменные | | **Очистка** | через декоратор / контекстный менеджер | автоматически после каждого теста | | **Использование в фикстурах** | неудобно (контекстный менеджер в фикстуре) | естественно (передать `mocker` как аргумент) | | **Базовые объекты** | `MagicMock`, `call` и т.д. | идентичны -- просто обёрнуты | Оба распространены в реальных pytest-проектах. Используйте `unittest.mock` напрямую, когда нужны нулевые дополнительные зависимости или пишете библиотечный код. Предпочитайте `pytest-mock` в выделенном pytest-проекте -- он хорошо вписывается в модель фикстур и устраняет ловушку с порядком аргументов декораторов при патчинге нескольких вещей.

unittest.mock и pytest-mock рядом

#
## Тестируемый код ```python # price.py import requests import time BASE_URL = 'https://api.market.example.com' def fetch_price(ticker): resp = requests.get(f'{BASE_URL}/price/{ticker}') resp.raise_for_status() return resp.json()['price'] def fetch_price_with_retry(ticker, retries=3): for attempt in range(retries): try: return fetch_price(ticker) except ConnectionError: if attempt < retries - 1: time.sleep(1) raise ConnectionError('all retries failed') ``` ## 1. Базовый мок -- один тест, два стиля ```python # Использование unittest.mock (декоратор @patch) from unittest.mock import patch import pytest, price @patch('price.requests.get') def test_fetch_price_stdlib(mock_get): mock_get.return_value.json.return_value = {'price': 150.0} mock_get.return_value.raise_for_status.return_value = None result = price.fetch_price('AAPL') assert result == 150.0 mock_get.assert_called_once_with(f'{price.BASE_URL}/price/AAPL') # Использование pytest-mock (фикстура mocker) def test_fetch_price_mocker(mocker): mock_get = mocker.patch('price.requests.get') mock_get.return_value.json.return_value = {'price': 150.0} mock_get.return_value.raise_for_status.return_value = None result = price.fetch_price('AAPL') assert result == 150.0 mock_get.assert_called_once_with(f'{price.BASE_URL}/price/AAPL') ``` Логика идентична. Единственные отличия: декоратор против параметра-фикстуры и способ получения мока. Объект `MagicMock` и все его методы утверждений одинаковы. ## 2. side_effect -- оба стиля ```python # stdlib @patch('price.requests.get') def test_fetch_raises_stdlib(mock_get): mock_get.side_effect = ConnectionError('network down') with pytest.raises(ConnectionError): price.fetch_price('AAPL') # pytest-mock def test_fetch_raises_mocker(mocker): mock_get = mocker.patch('price.requests.get') mock_get.side_effect = ConnectionError('network down') with pytest.raises(ConnectionError): price.fetch_price('AAPL') ``` ## 3. Множественные патчи -- где mocker выигрывает Тестирование логики повторных попыток требует мокирования и `requests.get`, и `time.sleep`. **stdlib -- стекированные декораторы, перевёрнутый порядок аргументов:** ```python from unittest.mock import patch, MagicMock import price @patch('price.requests.get') # <- применяется вторым (внешний) @patch('price.time.sleep') # <- применяется первым (внутренний) def test_retry_stdlib(mock_sleep, mock_get): # внутренний декоратор -> первый аргумент success = MagicMock() success.json.return_value = {'price': 99.0} success.raise_for_status.return_value = None mock_get.side_effect = [ConnectionError(), ConnectionError(), success] result = price.fetch_price_with_retry('AAPL') assert result == 99.0 assert mock_sleep.call_count == 2 ``` Перевёрнутый порядок аргументов (`mock_sleep` перед `mock_get`, хотя `@patch('...get')` идёт первым) -- широко известный источник багов. Легко перепутать молча -- оба мока являются объектами `MagicMock` без типа, который поймал бы подмену. **pytest-mock -- именованные переменные, естественный порядок:** ```python def test_retry_mocker(mocker): mock_get = mocker.patch('price.requests.get') mock_sleep = mocker.patch('price.time.sleep') success = mocker.MagicMock() success.json.return_value = {'price': 99.0} success.raise_for_status.return_value = None mock_get.side_effect = [ConnectionError(), ConnectionError(), success] result = price.fetch_price_with_retry('AAPL') assert result == 99.0 assert mock_sleep.call_count == 2 ``` Каждый мок имеет осмысленное имя. Добавление третьего патча -- просто ещё одна строка, без переупорядочивания декораторов и перемешивания аргументов. ## 4. patch.object -- оба стиля ```python class ApiClient: def get(self, url): ... # реальный сетевой вызов # stdlib -- нужен контекстный менеджер для патча на уровне экземпляра: def test_client_stdlib(): client = ApiClient() with patch.object(client, 'get', return_value={'data': 'ok'}) as mock_get: result = client.get('/resource') assert result == {'data': 'ok'} mock_get.assert_called_once_with('/resource') # pytest-mock -- нет контекстного менеджера, очистка автоматическая: def test_client_mocker(mocker): client = ApiClient() mock_get = mocker.patch.object(client, 'get', return_value={'data': 'ok'}) result = client.get('/resource') assert result == {'data': 'ok'} mock_get.assert_called_once_with('/resource') ``` ## 5. mocker внутри фикстуры ```python # conftest.py import pytest @pytest.fixture def mock_price_service(mocker): mock = mocker.patch('price.requests.get') mock.return_value.raise_for_status.return_value = None mock.return_value.json.return_value = {'price': 42.0} return mock # тестовый файл -- мок уже активен при запуске теста def test_uses_mock_fixture(mock_price_service): import price result = price.fetch_price('AAPL') assert result == 42.0 mock_price_service.assert_called_once() ``` Передача `mocker` в фикстуру естественна -- это просто ещё одна зависимость-фикстура. То же самое с `@patch` требует контекстного менеджера внутри фикстуры, что менее читаемо.

Краткий справочник по мокированию

#
**Установка pytest-mock:** ```bash pip install pytest-mock ``` --- **Сравнительная шпаргалка:** | Задача | `unittest.mock` | `pytest-mock` | |------|-----------------|---------------| | Патч имени | декоратор `@patch('mod.func')` | `mocker.patch('mod.func')` | | Патч только в части теста | `with patch('mod.func') as m:` | тот же `mocker.patch()` -- авто-очистка | | Патч метода экземпляра | `patch.object(obj, 'method')` | `mocker.patch.object(obj, 'method')` | | Создание автономного мока | `MagicMock()` | `mocker.MagicMock()` | | Несколько патчей | стекированные декораторы (перевёрнутые аргументы) | отдельные вызовы, именованные переменные | | Использование в фикстуре | контекстный менеджер в теле фикстуры | передать `mocker` как аргумент фикстуры | --- **return_value / side_effect (идентично в обоих):** ```python mock.return_value = 42 mock.side_effect = ValueError # выбрасывает класс исключения mock.side_effect = [1, 2, 3] # последовательный возврат mock.side_effect = lambda x: x * 2 # вызываемый -- вызывается с теми же аргументами ``` **Утверждения (идентично в обоих):** ```python mock.assert_called() mock.assert_called_once() mock.assert_called_with(*args, **kw) # последний вызов mock.assert_called_once_with(*args, **kw) # ровно один вызов mock.assert_not_called() mock.call_count # int mock.call_args_list # список всех вызовов ``` **Правило пути патча (одинаково в обоих):** Всегда патчите `'your_module.dependency'`, а не `'original_module.dependency'`. Для `from x import y` в вашем модуле: патчите `'your_module.y'`. **Несколько патчей с mocker:** ```python def test_something(mocker): mock_a = mocker.patch('mod.func_a') # у каждого понятное имя mock_b = mocker.patch('mod.func_b') # порядок не важен mock_c = mocker.patch('mod.func_c') ``` **Несколько патчей с @patch (перевёрнутые аргументы):** ```python @patch('mod.func_a') # крайний снаружи -> последний аргумент @patch('mod.func_b') @patch('mod.func_c') # крайний внутри -> первый аргумент def test_something(mc, mb, ma): # перевёрнуто! ... ```
01

Мок возвращаемого значения (unittest.mock)

#

У вас есть `get_discounted_price(product_id)` в `store.py`, которая вызывает `get_price(product_id)` и возвращает `round(price * 0.9, 2)`. Напишите тест, используя `@patch`, чтобы замокать `store.get_price` возвращающей `100.0`, и проверьте, что `get_discounted_price` возвращает `90.0`.

# store.py
def get_price(product_id):
    raise NotImplementedError('would call external API')

def get_discounted_price(product_id):
    price = get_price(product_id)
    return round(price * 0.9, 2)


# test_store.py
from unittest.mock import patch
import store


@patch('store.get_price')
def test_discounted_price(mock_get_price):
    # установить mock_get_price.return_value = 100.0
    # вызвать store.get_discounted_price('laptop')
    # проверить result == 90.0
    pass
Решение
# store.py
def get_price(product_id):
    raise NotImplementedError('would call external API')

def get_discounted_price(product_id):
    price = get_price(product_id)
    return round(price * 0.9, 2)


# test_store.py
from unittest.mock import patch
import store


@patch('store.get_price')
def test_discounted_price(mock_get_price):
    mock_get_price.return_value = 100.0

    result = store.get_discounted_price('laptop')

    assert result == 90.0
02

Мок requests.get (unittest.mock)

#

У вас есть `fetch_user(user_id)` в `fetcher.py`, которая вызывает `requests.get` по адресу `https://api.example.com/users/{user_id}` и возвращает `resp.json()`. Напишите тест с `@patch`, чтобы не было реального HTTP-вызова. Проверьте возвращаемое значение и что `requests.get` был вызван с правильным URL.

# fetcher.py
import requests

def fetch_user(user_id):
    resp = requests.get(f'https://api.example.com/users/{user_id}')
    return resp.json()


# test_fetcher.py
from unittest.mock import patch
import fetcher


@patch('fetcher.requests.get')
def test_fetch_user(mock_get):
    # настроить: mock_get.return_value.json.return_value = {...}
    # вызвать fetcher.fetch_user(1)
    # проверить результат и URL
    pass
Решение
# fetcher.py
import requests

def fetch_user(user_id):
    resp = requests.get(f'https://api.example.com/users/{user_id}')
    return resp.json()


# test_fetcher.py
from unittest.mock import patch
import fetcher


@patch('fetcher.requests.get')
def test_fetch_user(mock_get):
    mock_get.return_value.json.return_value = {'id': 1, 'name': 'Alice'}

    result = fetcher.fetch_user(1)

    assert result == {'id': 1, 'name': 'Alice'}
    mock_get.assert_called_once_with('https://api.example.com/users/1')
03

Проверка аргументов вызова (pytest-mock)

#

Перепишите тот же сценарий `notify_user` с использованием pytest-mock. У вас есть `notify_user(user, message)` в `notifier.py`, которая вызывает `send_sms(phone, message)`. Напишите тест, используя фикстуру `mocker` (без `@patch`), которая патчит `notifier.send_sms` и проверяет, что она была вызвана ровно один раз с правильным номером телефона и сообщением. Установите pytest-mock: `pip install pytest-mock`.

# notifier.py
def send_sms(phone, message):
    raise NotImplementedError('calls external SMS gateway')

def notify_user(user, message):
    send_sms(user['phone'], message)


# test_notifier.py
import notifier


def test_notify_calls_sms_with_correct_args(mocker):
    # mock_send = mocker.patch('notifier.send_sms')
    # вызвать notifier.notify_user со словарём пользователя и сообщением
    # mock_send.assert_called_once_with(...)
    pass
Решение
# notifier.py
def send_sms(phone, message):
    raise NotImplementedError('calls external SMS gateway')

def notify_user(user, message):
    send_sms(user['phone'], message)


# test_notifier.py
import notifier


def test_notify_calls_sms_with_correct_args(mocker):
    mock_send = mocker.patch('notifier.send_sms')
    user = {'name': 'Alice', 'phone': '+380501234567'}

    notifier.notify_user(user, 'Your order is ready')

    mock_send.assert_called_once_with('+380501234567', 'Your order is ready')
04

side_effect для выброса исключения (pytest-mock)

#

У вас есть `safe_fetch(url)` в `client.py`, которая оборачивает `requests.get` в try/except: при успехе возвращает `resp.json()`, при `ConnectionError` возвращает `None`. Напишите тест с фикстурой `mocker`, который устанавливает `side_effect = ConnectionError(...)` на замокированный `requests.get` и проверяет, что `safe_fetch` возвращает `None`.

# client.py
import requests

def safe_fetch(url):
    try:
        resp = requests.get(url, timeout=5)
        return resp.json()
    except ConnectionError:
        return None


# test_client.py
import client


def test_safe_fetch_returns_none_on_error(mocker):
    # mock_get = mocker.patch('client.requests.get')
    # установить mock_get.side_effect = ConnectionError(...)
    # вызвать client.safe_fetch(...)
    # проверить result is None
    pass
Решение
# client.py
import requests

def safe_fetch(url):
    try:
        resp = requests.get(url, timeout=5)
        return resp.json()
    except ConnectionError:
        return None


# test_client.py
import client


def test_safe_fetch_returns_none_on_error(mocker):
    mock_get = mocker.patch('client.requests.get')
    mock_get.side_effect = ConnectionError('network unreachable')

    result = client.safe_fetch('https://api.example.com/data')

    assert result is None
05

Мок метода экземпляра через mocker.patch.object

#

У вас есть `EmailService` с методом `send(to, subject, body)` и `UserRegistration`, которая вызывает его в `register(user)`. Напишите тест с `mocker.patch.object`, чтобы замокать метод `send` на конкретном экземпляре `EmailService`. Проверьте, что `register` возвращает `True` и что `send` был вызван с правильными именованными аргументами.

# registration.py

class EmailService:
    def send(self, to, subject, body):
        raise NotImplementedError('sends real email in production')

class UserRegistration:
    def __init__(self, email_service):
        self.email_service = email_service

    def register(self, user):
        self.email_service.send(
            to=user['email'],
            subject='Welcome!',
            body=f"Hello {user['name']}, your account is ready.",
        )
        return True


# test_registration.py
from registration import EmailService, UserRegistration


def test_registration_sends_welcome_email(mocker):
    email_service = EmailService()
    registration = UserRegistration(email_service)
    user = {'name': 'Bob', 'email': '[email protected]'}

    # mock_send = mocker.patch.object(email_service, 'send')
    # вызвать registration.register(user)
    # проверить result is True
    # проверить mock_send.assert_called_once_with(to=..., subject=..., body=...)
    pass
Решение
# registration.py

class EmailService:
    def send(self, to, subject, body):
        raise NotImplementedError('sends real email in production')

class UserRegistration:
    def __init__(self, email_service):
        self.email_service = email_service

    def register(self, user):
        self.email_service.send(
            to=user['email'],
            subject='Welcome!',
            body=f"Hello {user['name']}, your account is ready.",
        )
        return True


# test_registration.py
from registration import EmailService, UserRegistration


def test_registration_sends_welcome_email(mocker):
    email_service = EmailService()
    registration = UserRegistration(email_service)
    user = {'name': 'Bob', 'email': '[email protected]'}

    mock_send = mocker.patch.object(email_service, 'send')

    result = registration.register(user)

    assert result is True
    mock_send.assert_called_once_with(
        to='[email protected]',
        subject='Welcome!',
        body='Hello Bob, your account is ready.',
    )