0

How do you patch a method in a class and then assert that that patched method was only called once?

For example:

import typing
import unittest
import unittest.mock

class Example:
    def __init__(self: "Example") -> None:
        self._loaded : bool = False
        self._data : typing.Union[str,None] = None

    def data(self: "Example") -> str:
        if not self._loaded:
            self.load()
        return self._data

    def load(self: "Example") -> None:
        self._loaded = True
        # some expensive computations
        self._data = "real_data"

def mocked_load(self: "Example") -> None:
    # mock the side effects of load without the expensive computation.
    self._loaded = True
    self._data = "test_data"

class TestExample( unittest.TestCase ):
    def test_load(self: "TestExample") -> None:
        # tests for the expensive computations
        ...

    @unittest.mock.patch("__main__.Example.load", new=mocked_load)
    def test_data(
        self: "TestExample",
        # patched_mocked_load: unittest.mock.Mock
    ) -> None:
        example = Example()
        data1 = example.data()
        self.assertEqual(data1, "test_data")
        # example.load.assert_called_once()
        # Example.load.assert_called_once()
        # mocked_load.assert_called_once()
        # patched_mocked_load.assert_called_once()

        data2 = example.data()
        self.assertEqual(data2, "test_data")
        # Should still only have loaded once.
        # example.load.assert_called_once()
        # Example.load.assert_called_once()
        # mocked_load.assert_called_once()
        # patched_mocked_load.assert_called_once()

if __name__ == '__main__':
    unittest.main()

How in the test_data unit test do I assert that the patched load function was only called once?

MT0
  • 143,790
  • 11
  • 59
  • 117

1 Answers1

1

The problem is that you substitute load with your own function, which is not a mock and therefore does not have assert_called_xxx methods.
You need to substitute it with a mock and add the wanted behavior to the mock instead:

    @unittest.mock.patch("__main__.Example.load")
    def test_data(
            self: "TestExample",
            patched_mocked_load: unittest.mock.Mock
    ) -> None:
        example = Example()
        patched_mocked_load.side_effect = lambda: mocked_load(example)
        data1 = example.data()
        self.assertEqual(data1, "test_data")
        patched_mocked_load.assert_called_once()

        data2 = example.data()
        self.assertEqual(data2, "test_data")
        patched_mocked_load.assert_called_once()
MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46
  • This works but it feels like having to use a work-around as you're passing the class instance directly into the side effect lambda and that this would not be a viable solution if there were two instances of the Example class within the test as which instance the `self` refers to in the `mocked_load` function is effectively hardcoded rather than being passed dynamically with the call. – MT0 Jun 18 '20 at 16:06
  • Agreed, I didn't like it either, but couldn't come up with something better... probably missing something. – MrBean Bremen Jun 18 '20 at 16:49