Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Checking call order across multiple mocks

I have three functions that I'm trying to test the call order of.

Let's say that in module module.py I have the following

# module.py      def a(*args):     # do the first thing  def b(*args):     # do a second thing  def c(*args):     # do a third thing   def main_routine():     a_args = ('a')     b_args = ('b')     c_args = ('c')      a(*a_args)     b(*b_args)     c(*c_args) 

I want to check that b is called after a, and before c. So getting a mock for each of a, b and c is easy:

# tests.py  @mock.patch('module.a') @mock.patch('module.b') @mock.patch('module.c') def test_main_routine(c_mock, b_mock, a_mock):     # test all the things here 

Checking that each of the individial mocks are called is easy, too. How do I check the order of the calls relative to one another?

call_args_list won't work as it's maintained separately for each mock.

I've tried using a side effect to log each of the calls:

calls = [] def register_call(*args):     calls.append(mock.call(*args))     return mock.DEFAULT  a_mock.side_effect = register_call b_mock.side_effect = register_call c_mock.side_effect = register_call 

But this only gives me the args that the mocks were called with, but not the actual mock that the call was made against. I can add a bit more logic:

# tests.py from functools import partial  def register_call(*args, **kwargs):     calls.append(kwargs.pop('caller', None), mock.call(*args, **kwargs))     return mock.DEFAULT  a_mock.side_effect = partial(register_call, caller='a') b_mock.side_effect = partial(register_call, caller='b') c_mock.side_effect = partial(register_call, caller='c') 

And that seems to get the job done... Is there a better way though? It feels like there should already be something in the API that can do this that I'm missing.

like image 994
Shaun O'Keefe Avatar asked Mar 27 '14 02:03

Shaun O'Keefe


People also ask

What is Assert_has_calls?

assert_has_calls (calls, any_order=False) assert the mock has been called with the specified calls. The mock_calls list is checked for the calls. If any_order is False (the default) then the calls must be sequential. There can be extra calls before or after the specified calls.

How do you assert called twice in Python?

assertEqual(mock_call_me. call_count, 2) as mentioned above.

What is mock Pytest?

In pytest , mocking can replace the return value of a function within a function. This is useful for testing the desired function and replacing the return value of a nested function within that desired function we are testing.


2 Answers

Define a Mock manager and attach mocks to it via attach_mock(). Then check for the mock_calls:

@patch('module.a') @patch('module.b') @patch('module.c') def test_main_routine(c, b, a):     manager = Mock()     manager.attach_mock(a, 'a')     manager.attach_mock(b, 'b')     manager.attach_mock(c, 'c')      module.main_routine()      expected_calls = [call.a('a'), call.b('b'), call.c('c')]     assert manager.mock_calls == expected_calls 

Just to test that it works, change the order of function calls in the main_routine() function add see that it throws AssertionError.

See more examples at Tracking order of calls and less verbose call assertions

Hope that helps.

like image 106
alecxe Avatar answered Sep 24 '22 03:09

alecxe


I needed this answer today, but the example code in the question is really hard to read because the call args are the same as the names of the mocks on the manager and in the scope of the test. Here's the official documentation on this concept, and below is a clearer example for non-robots. All the modules I'm patching are made-up for the sake of the example:

@patch('module.file_reader') @patch('module.json_parser') @patch('module.calculator') def test_main_routine(mock_calculator, mock_json_parser, mock_file_reader):     manager = Mock()      # First argument is the mock to attach to the manager.     # Second is the name for the field on the manager that holds the mock.     manager.attach_mock(mock_file_reader, 'the_mock_file_reader')     manager.attach_mock(mock_json_parser, 'the_mock_json_parser')     manager.attach_mock(mock_calculator, 'the_mock_calculator')          module.main_routine()      expected_calls = [         call.the_mock_file_reader('some file'),         call.the_mock_json_parser('some json'),         call.the_mock_calculator(1, 2)     ]     assert manager.mock_calls == expected_calls 

Note that you have to use attach_mock in this case because your mocks were created by patch. Mocks with names, including those created by patch, must be attached via attach_mock for this code to work. You don't have to use attach_mock if you make your own Mock objects without names:

def test_main_routine(mock_calculator, mock_json_parser, mock_file_reader):     manager = Mock()      mock_file_reader = Mock()     mock_json_parser = Mock()     mock_calculator = Mock()      manager.the_mock_file_reader = mock_file_reader     manager.the_mock_json_parser = mock_json_parser     manager.the_mock_calculator = mock_calculator          module.main_routine()      expected_calls = [         call.the_mock_file_reader('some file'),         call.the_mock_json_parser('some json'),         call.the_mock_calculator(1, 2)     ]     assert manager.mock_calls == expected_calls 

If you want a clear assertion failed message when the order or expected calls are missing, use the following assert line instead.

self.assertListEqual(manager.mock_calls, [     call.the_mock_file_reader('some file'),     call.the_mock_json_parser('some json'),     call.the_mock_calculator(1, 2) ]) 
like image 28
lortimer Avatar answered Sep 22 '22 03:09

lortimer