2025, Nov 30 09:00

Mocking Python Classes Correctly: Patch __init__ and get_component to Keep Unit Tests Fast and Stable

Fix flaky Python unit tests by patching __init__ and methods with unittest.mock, preventing network calls in CI and keeping tests fast, isolated, and reliable.

Unit tests should be fast, isolated and boring. If they try to talk to the outside world, they quickly become flaky, especially in CI. A common trap is patching the method you plan to call while forgetting that the class constructor triggers network activity first. That’s exactly what happened in a test targeting lauApiSession: the test mocked get_component, but the session’s initialization still attempted to reach a login endpoint and failed on GitHub Actions.

Problem demonstration

The test below aims to mock out get_component and verify the return value, but it still performs a real connection during construction:

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()

What’s really going on

The logs tell the story: during construction the session announces that it’s retrieving a token from the lau API, then errors out trying to connect to mock-lau-login-url. Only get_component was patched; the constructor wasn’t. Because the initialization performs the token retrieval, the test hit the network before the mocked method ever ran, which explains the failure in CI.

The fix

Patch both the constructor and the method you’re exercising. By stubbing __init__ to return None, you prevent any side effects during object creation. Then you can safely mock get_component and assert behavior without network calls.

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()

Why this matters

Tests that implicitly perform I/O are brittle and slow, and they surprise you in CI with timeouts or DNS errors. By explicitly patching the constructor whenever it performs external setup, you keep unit tests hermetic, reproducible and focused on contract, not on infrastructure availability.

Takeaways

If a class performs work on construction, treat the constructor like any other side-effecting method: patch it in tests that don’t need the real behavior. Then mock only the method you want to validate. This prevents accidental calls to live services, stabilizes your GitHub Actions jobs and keeps your unit tests truly unit-sized.