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 и сохраняет модульные тесты по‑настоящему «модульными».