Search code examples
pythonmockingpytestpython-unittest

Mocking a module function and it's return


I hope this is a straightforward answer, but I've been stuck trying to sort this out. I'm looking to mock out the following scenario. I previously had something like this that worked:

# path/to/some/module.py

class MyClass:
    # None of the methods or init should ever run in a test
    def __init__():
        pass

    def do_thing():
        pass

def get_my_class():
    return MyClass()
# path/to/some/other/module.py

from path.to.some.module import get_my_class()

def do_something()
    get_my_class().do_thing()
    return 'something'
# test/path/to/some/other/module.py

from path.to.some.other.module import 

def test_do_something():
    with patch("path.to.some.module.MyClass") as mock:
        assert do_something() = 'something'
        mock().do_thing.assert_called_once()

So that was fine - the asserts worked, and the init and class methods were not called. But then I needed to move around some logic, and now I am unable to sort out how to get this to work from a mocking standpoint. The code itself works, I am just unable to get my mocks in a row. See below for the latest structure:

# path/to/some/module.py

class MyClass:
    # None of the methods or init should ever run in a test
    def __init__():
        pass

    def do_thing():
        pass


class MyClassSingleton:
    _my_class = None


def get_my_class():
    if MyClassSingleton._my_class is None:
        MyClassSingleton._my_class = MyClass()
    return MyClassSingleton._my_class
# path/to/some/other/module.py

from path.to.some.module import get_my_class()

def do_something()
    get_my_class().do_thing()
    return 'something'

I tried updating my test to be something like:

with patch("path.to.some.module.MyClassSingleton._my_class") as mock:

but this leads to the MyClass.__init__ code running, which is no bueno.

My goal is for everything to work more or less as before from a mocking standpoint, ideally with as simple a setup/boilerplate as possible since I need to apply these changes to hundreds of tests. Any help would be appreciated. Thanks!


Solution

  • It seems like the easiest answer was to create a patch for the singleton, but also mock out the _client reference as well. So I made a fixture like this:

    
    @pytest.fixture
    def my_client() -> MagicMock:
        """
        Provide a mocked client via singleton that does not initialize
        """
        mock_provider = MagicMock()
    
        with patch("path.to.some.module.MyClassSingleton", mock_provider):
            yield mock_provider._client
    
    def test_do_something(my_client):
        assert do_something() = 'something'
        mock.do_thing.assert_called_once()
    

    It is possible I could have mocked out get_my_class, but as I was moving towards a fixture-based approach, this proved to be less desirable, as I would import get_my_class into various modules, which meant the patch target reference to get_my_class would have to change, which would mean parameterizing fixtures which I didn't want to chase down.

    Because the provider/singleton class was only accessed via get_my_class, by mocking the provider I was able to have a single fixture that would work everywhere.