Найти в Дзене
Анастасия Софт

Мокаем всё, что шевелится: как и когда использовать mock в Python

Оглавление
Мокаем всё, что шевелится: как и когда использовать mock в Python
Мокаем всё, что шевелится: как и когда использовать mock в Python

Когда ты пишешь тесты для Python-приложений, наверняка сталкивался с необходимостью заменить какую-то сложную или внешнюю зависимость на её "мок". Возможно, ты думал: "Зачем мне мокать? Всё и так работает!" Однако, как только ты начинаешь работать с реальными сервисами, базами данных или внешними API, ты понимаешь, что тесты могут стать медленными, нестабильными и уязвимыми для сбоев этих зависимостей.

И вот тут на сцену выходит mock. Этот инструмент позволяет создавать замену для реальных объектов, что значительно ускоряет тестирование, улучшает стабильность и помогает тестировать в изоляции.

В этой статье мы разберемся, как и когда использовать unittest.mock (или просто mock), научимся патчить внешние зависимости, мокать API, время, файловую систему и другие компоненты. А также узнаем, когда лучше не мокать.

1. Что такое mock и зачем он нужен?

Когда мы тестируем, мы часто сталкиваемся с ситуацией, когда нам нужно изолировать тестируемую единицу от внешних зависимостей. Например, если наш код обращается к базе данных, внешним API или использует время — все эти компоненты могут вызвать непредсказуемые результаты, замедлить тесты или просто сделать их ненадежными.

mock помогает нам создать замену для этих компонентов, что позволяет тестировать код в чистой изоляции.

Пример 1: Зачем мокать базу данных

Предположим, что у нас есть функция, которая записывает данные в базу данных, и мы не хотим, чтобы тесты зависели от реальной базы данных. Вместо этого мы можем замокать соединение с базой данных.

import unittest
from unittest.mock import MagicMock

# Пример функции, которая записывает данные в базу
def save_to_database(data, db_connection):
db_connection.execute("INSERT INTO table (data) VALUES (?)", (data,))
db_connection.commit()

# Тест с мокированием db_connection
class TestSaveToDatabase(unittest.TestCase):
def test_save_data(self):
mock_db = MagicMock()
save_to_database("Test Data", mock_db)

# Проверяем, что метод execute был вызван с нужными параметрами
mock_db.execute.assert_called_with("INSERT INTO table (data) VALUES (?)", ("Test Data",))

if __name__ == '__main__':
unittest.main()

Комментарий к коду:

  • Мы создаем мок-объект для подключения к базе данных (mock_db) с помощью MagicMock().
  • В тесте мы проверяем, что метод execute был вызван с правильными параметрами.
  • Это позволяет нам тестировать логику без необходимости взаимодействовать с реальной базой данных.

Совет: Мокать внешние зависимости, такие как база данных или внешние API, особенно когда тесты работают с удалёнными сервисами, — это просто необходимость. Это ускоряет тесты и делает их более стабильными.

2. Патчинг внешних зависимостей

Когда ты пишешь тесты для сложных систем, тебе не всегда нужно мокать всё вручную. Вместо этого можно использовать патчинг — замену реальных объектов на моки прямо в момент их использования. unittest.mock.patch — это мощный инструмент для патчинга.

Пример 2: Патчим вызов внешнего API

Предположим, у нас есть функция, которая делает запрос к внешнему API. Мы можем использовать патчинг, чтобы заменить этот запрос на мок.

import requests
import unittest
from unittest.mock import patch

# Функция, которая делает запрос к API
def get_weather(city):
response = requests.get(f"http://weatherapi.com/{city}")
return response.json()

# Тест с патчингом внешнего API
class TestGetWeather(unittest.TestCase):
@patch('requests.get') # Патчим requests.get
def test_get_weather(self, mock_get):
# Настроим мок, чтобы он возвращал ожидаемый результат
mock_get.return_value.json.return_value = {"temperature": 22}

result = get_weather("Moscow")

# Проверяем, что результат корректный
self.assertEqual(result, {"temperature": 22})

if __name__ == '__main__':
unittest.main()

Комментарий к коду:

  • Мы используем patch для того, чтобы заменить вызов requests.get на мок. Это позволяет нам протестировать логику без выполнения настоящего запроса.
  • Мы также настраиваем мок, чтобы он возвращал определенный результат при вызове json().

Совет: Когда ты работаешь с внешними сервисами, патчинг позволяет избежать лишних HTTP-запросов и контролировать возвращаемые данные.

3. Мокаем время

Время — ещё одна зависимость, которую часто нужно мокать. Например, если твой код зависит от текущей даты или времени, можно использовать mock для замены времени в тестах.

Пример 3: Мокаем время с помощью patch

Предположим, что у нас есть функция, которая проверяет, прошло ли больше суток с какого-то события. Мы можем замокать время, чтобы точно проверить, как работает эта логика.

import datetime
import unittest
from unittest.mock import patch

# Функция, которая проверяет, прошло ли больше суток
def has_24_hours_passed(last_event_time):
return datetime.datetime.now() - last_event_time > datetime.timedelta(days=1)

# Тест с мокацией времени
class TestHas24HoursPassed(unittest.TestCase):
@patch('datetime.datetime') # Патчим datetime.datetime
def test_has_24_hours_passed(self, mock_datetime):
# Настроим мок, чтобы текущая дата была 2025-04-22
mock_datetime.now.return_value = datetime.datetime(2025, 4, 22, 12, 0, 0)
last_event_time = datetime.datetime(2025, 4, 21, 10, 0, 0)

result = has_24_hours_passed(last_event_time)

self.assertTrue(result) # Проверяем, что прошло более суток

if __name__ == '__main__':
unittest.main()

Комментарий к коду:

  • Мы используем patch для того, чтобы замокать datetime.datetime.now(), чтобы всегда получать фиксированное время.
  • Это помогает нам точно контролировать логику, которая зависит от времени, и не зависит от реального времени при выполнении тестов.

Совет: Мокать время — это очень полезная практика, когда твой код зависит от текущего времени или дат. Так ты можешь точно управлять тестами и проверять логику, связанную с временными ограничениями.

4. Мокаем файловую систему

Иногда нужно взаимодействовать с файловой системой, и для этого тоже можно использовать mock, чтобы избежать работы с реальными файлами.

Пример 4: Мокаем работу с файлами

Предположим, у нас есть функция, которая записывает данные в файл. Мы можем использовать mock для имитации этой работы.

import unittest
from unittest.mock import mock_open, patch

# Функция, которая записывает данные в файл
def write_to_file(file_path, data):
with open(file_path, 'w') as file:
file.write(data)

# Тест с мокацией файловой системы
class TestWriteToFile(unittest.TestCase):
@patch('builtins.open', new_callable=mock_open) # Патчим open для мока
def test_write_to_file(self, mock_file):
write_to_file('test.txt', 'Hello, World!')

# Проверяем, что файл был открыт и записаны правильные данные
mock_file.assert_called_once_with('test.txt', 'w')
mock_file().write.assert_called_once_with('Hello, World!')

if __name__ == '__main__':
unittest.main()

Комментарий к коду:

  • Мы используем patch и mock_open, чтобы замокать вызов open и избежать реального создания и записи в файл.
  • Мы проверяем, что файл был открыт с правильными параметрами и что в него были записаны корректные данные.

Совет: Мокация файловой системы помогает избежать проблем с правами доступа или зависимостями от наличия файлов на реальной файловой системе.

5. Подводные камни: когда лучше не мокать

Хотя mock — это мощный инструмент, его не всегда следует использовать. Вот несколько случаев, когда лучше не мокать:

1. Когда тесты становятся слишком сложными

Mocking — это инструмент, который упрощает изоляцию тестов, но если ты слишком увлекся его использованием, тесты могут стать сложными для понимания. Когда ты начинаешь моксить всё подряд, это может привести к ситуации, когда невозможно понять, что же на самом деле происходит в тестируемом коде.

Пример:
Если твой тест зависит от множества моков и патчей, это может запутать не только тебя, но и других разработчиков. В такой ситуации тесты становятся не проверкой логики, а просто проверкой того, что ты правильно замокал все зависимости. Это уже не тестирование, а скорее имитация.

2. Когда мокать — это обходной путь, а не решение проблемы

Иногда мы используем mock, потому что не хотим или не можем разобраться в реальной логике взаимодействия с внешними сервисами, базами данных или даже временем. Однако если ты постоянно замещаешь настоящие компоненты, не пытаясь решить проблему по-настоящему (например, не покрываешь тестами взаимодействие с реальной базой данных), это может привести к тому, что твои тесты будут неэффективными.

3. Когда ты тестируешь взаимодействие с внешними системами

Мокать взаимодействие с внешними системами вроде API, базы данных или файловой системы — это хорошо, но важно помнить, что иногда тесты должны проверять реальное взаимодействие. Например, если ты тестируешь интеграцию с внешним API, важно, чтобы тесты действительно проверяли, как твое приложение работает с этим API, а не только имитировало его работу с помощью mock. Иногда стоит оставлять реальный API на месте (например, с ограничением количества запросов или с использованием мок-сервера) для интеграционных тестов.

4. Когда ты не уверен в том, как что-то работает

Mocking — это подход, который следует использовать только тогда, когда ты четко понимаешь, что и как нужно замокать. Если ты начинаешь мокать всё подряд, не понимая сути проблемы, тесты могут стать ненадежными. Лучше потратить время на понимание настоящей логики и зависимостей, чем заменять их на моки, создавая иллюзию покрытия.

Заключение

Мокирование — это один из самых мощных инструментов для тестирования в Python. Использование unittest.mock и его возможностей позволяет изолировать код от внешних зависимостей, ускоряя тесты и делая их более стабильными. Мы рассмотрели множество примеров, включая мокацию баз данных, времени, API, файловой системы и многое другое.

Однако важно помнить, что mock — это не панацея. Используй его осознанно и в тех случаях, когда это действительно нужно. Если ты начинаешь мокирать всё подряд без разбора, это может привести к тому, что тесты будут менее полезными, а их поддержка — более сложной. Всегда проверяй, чтобы твои тесты отражали реальное поведение кода и системы в целом.

Так что вперед, моки все, что шевелится, но делай это с умом!

-2