NiegodziwyBeru
NiegodziwyBeru

Reputation: 864

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

Upvotes: 5

Views: 2768

Answers (3)

Cilyan
Cilyan

Reputation: 8481

Another possibility:

Checking call_count individually on restricted methods

Ensure that call_count is zero on methods that should not be called.

class TestAlgorithm(unittest.TestCase):
    def test_algorithm(self):
        actionMock = mock.create_autospec(Action)
        self.assertTrue(algorithm(actionMock))
        actionMock.step1.assert_called_once_with('111')
        self.assertEqual(actionMock.step2.call_count, 0) # <<< like this

The drawback is that you have to check all unexpected calls one by one.

Upvotes: 0

J0HN
J0HN

Reputation: 26931

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. :)

Upvotes: 6

Simeon Visser
Simeon Visser

Reputation: 122356

Yes, this is possible using the spec= and autospec= arguments. See the mock documentation on Autospeccing for more information. In your example it would become:

action_mock = mock.Mock(spec=Action)

or:

action_mock = mock.Mock('Action', autospec=True)

Upvotes: 0

Related Questions