pythonpytestmagicmock

Testing ContextManager, Callable and MagicMock


I'm attempting to write a unit test using MagicMock.

I have 2 classes that are being tested:

class HelperClass:
    @property
    def output(self) -> dict[str, Any]:
        return self._output

    def __enter__(self):
        print("__enter__")
        self._output: dict[str, Any] = {}
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        print("__exit__")


class ImportantClass:
    def fn(self, key: str):
        with self._fac() as hc:
            if key not in hc.output:
                raise RuntimeError("bad bad not good")

            return hc.output[key]

    def __init__(self, fac: Callable[[], HelperClass]):
        self._fac = fac

ImportantClass is initialized with a factory method that can be used to create instances of HelperClass. Here's the test code

def test_important_class():
    hc = MagicMock(spec=HelperClass)
    omock = PropertyMock(return_value={"foo": "bar", "baz": "quux"})
    type(hc).output = omock

    assert hc.output["foo"] == "bar"
    assert hc.output["baz"] == "quux" # these assertions succeed

    assert OuterClass(fac=lambda: hc).fn("foo") == "bar"
    assert OuterClass(fac=lambda: hc).fn("baz") == "quux" # these don't

When I run through the code in the debugger the type of hc.output while in the test is dict[str: str], but when I step into the fn method, the type of hc.output is MagicMock.


Update I found this question which shows that the issue I'm facing is related to the implicit ContextManager that is being created when calling with self._fac() as hc.

I'm still having a tough time updating the test, so that the ContextManager gets mocked appropriately.


Solution

  • Finally got it, I had to create a method that could be patched in the unit test

    def get_mtc(self) -> MyTestClass: # this is a dummy method that will get patched
        return MyTestClass()
    
    
    def test_test_class():
        patcher = patch("my_unit_tests.get_mtc")
        fty = patcher.start()
        fty.return_value.__enter__.return_value.output = {"foo": "bar", "baz": "quux"} # mocks the implicit ContextManager
    
        assert OuterClass(fty).fn("foo") == "bar"
        assert OuterClass(fty).fn("baz") == "quux"
        patcher.stop()
    

    I'd be very interested to learn a more pythonic way of doing all of this.