2025, Dec 15 00:01
Как изолировать модульные тесты в Python: мокайте __init__ и методы без сети
Почему модульные тесты в Python падают в CI из‑за сетевых вызовов, и как это исправить: мок __init__ и get_component, изоляция I/O, стабильность GitHub Actions.
Модульные тесты должны быть быстрыми, изолированными и — в хорошем смысле — скучными. Если они обращаются к внешнему миру, они быстро становятся нестабильными, особенно в CI. Частая ловушка — замокать метод, который вы собираетесь вызвать, и забыть, что конструктор класса первым делом запускает сетевую активность. Ровно это и произошло в тесте для lauApiSession: замокали get_component, но при инициализации сессия всё равно попыталась обратиться к конечной точке логина и упала в GitHub Actions.
Демонстрация проблемы
Приведённый ниже тест пытается замокать get_component и проверить возвращаемое значение, но во время создания объекта всё равно происходит реальное подключение:
import unittest
from unittest.mock import patch, MagicMock
from alm_commons_utils.mylau.lau_client import lauApiSession as ApiSession
class TestApiSessionSpec(unittest.TestCase):
@patch("alm_commons_utils.mylau.lau_client.lauApiSession.get_component")
def test_component_fetch_is_mocked(self, fake_get):
fake_get.return_value = {"component": "mock_component"}
conn = ApiSession(
ldap_user="mock_user",
ldap_password="mock_password",
lau_api_url="https://mock-lau-api-url",
lau_login_url="https://mock-lau-login-url"
)
payload = conn.get_component("mock_repo")
fake_get.assert_called_once_with("mock_repo")
self.assertEqual(payload, {"component": "mock_component"})
if __name__ == "__main__":
unittest.main()
Что происходит на самом деле
Логи всё объясняют: во время создания объекта сессия сообщает, что получает токен у lau API, после чего падает при попытке подключиться к mock-lau-login-url. Подменили только get_component; конструктор не тронули. Поскольку инициализация занимается получением токена, тест вышел в сеть ещё до запуска замоканного метода — отсюда и сбой в CI.
Решение
Патчьте и конструктор, и метод, поведение которого проверяете. Если заглушить __init__, вернув None, вы уберёте любые побочные эффекты при создании объекта. Затем можно безопасно мокать get_component и проверять логику без сетевых вызовов.
import unittest
from unittest.mock import patch
from alm_commons_utils.mylau.lau_client import lauApiSession as ApiSession
class TestApiSessionSpec(unittest.TestCase):
def test_component_fetch_with_init_stub(self):
with patch("alm_commons_utils.mylau.lau_client.lauApiSession.__init__", return_value=None) as fake_ctor, \
patch("alm_commons_utils.mylau.lau_client.lauApiSession.get_component") as fake_get:
client = ApiSession(
ldap_user="mock_user",
ldap_password="mock_password",
lau_api_url="https://mock-lau-api-url",
lau_login_url="https://mock-lau-login-url",
)
fake_get.return_value = {"component": "mock_component"}
output = client.get_component("mock_repo")
fake_ctor.assert_called_once()
fake_get.assert_called_once_with("mock_repo")
self.assertEqual(output, {"component": "mock_component"})
if __name__ == "__main__":
unittest.main()
Почему это важно
Тесты, которые неявно выполняют I/O, получаются хрупкими и медленными, а в CI они неожиданно срываются из‑за таймаутов или ошибок DNS. Если явно патчить конструктор всякий раз, когда он выполняет внешнюю настройку, модульные тесты остаются герметичными, воспроизводимыми и сосредоточенными на контракте, а не на доступности инфраструктуры.
Выводы
Если класс выполняет работу при создании, относитесь к конструктору как к любому методу с побочными эффектами: патчьте его в тестах, где не нужен реальный запуск. Затем мокайте только тот метод, поведение которого хотите проверить. Это предотвращает случайные обращения к живым сервисам, стабилизирует ваши задания в GitHub Actions и сохраняет модульные тесты по‑настоящему «модульными».