Search code examples
pythonunit-testingmockingpython-mock

Strict mock in python


Is there any equivalent of strict mocks in python? Some mechanism to report unintended call of mocked methods (action.step2() in this example), just like this in GoogleMock framework.

class Action:
    def step1(self, arg):
        return False

    def step2(self, arg):
        return False

def algorithm(action):
    action.step1('111')
    action.step2('222')
    return True

class TestAlgorithm(unittest.TestCase):
    def test_algorithm(self):
        actionMock = mock.create_autospec(Action)
        self.assertTrue(algorithm(actionMock))
        actionMock.step1.assert_called_once_with('111')

Solution

  • Looks like it's not supported out of the box. However there are at least two approaches on how to achieve the same result.

    Passing list of allowed members

    According to mock documentation

    spec: This can be either a list of strings or an existing object (a class or instance) that acts as the specification for the mock object. If you pass in an object then a list of strings is formed by calling dir on the object (excluding unsupported magic attributes and methods). Accessing any attribute not in this list will raise an AttributeError.

    So, in order to fail your test example just replace

    actionMock = mock.create_autospec(Action)
    

    to

    actionMock = mock.Mock(spec=['step1'])
    

    Such an approach have certain drawbacks compared to passing class or instance as spec argument, as you have to pass all the allowed methods and than set up expectations on them, effectively registering them twice. Also, if you need to restrict a subset of methods you have to pass list of all methods execept those. This can be achieved as follows:

    all_members = dir(Action)  # according to docs this is what's happening behind the scenes
    all_members.remove('step2')  # remove all unwanted methods 
    actionMock = mock.Mock(spec=all_members)
    

    Setting exceptions on restricted methods

    Alternative approach would be to excplicitly set failures on methods you don't want to be called:

    def test_algorithm(self):
        actionMock = mock.create_autospec(Action)
        actionMock.step2.side_effect = AttributeError("Called step2") # <<< like this
        self.assertTrue(algorithm(actionMock))
        actionMock.step1.assert_called_once_with('111')
    

    This have some limitations as well: you've got to set errors as well as expectations.

    As a final note, one radical solution to the problem would be to patch mock to add strict parameter to Mock constructor and send a pull request. Than either it would be accepted or mock maintainers will point out on how to achieve that. :)