Test assert function order with mocks (pytest -> assert_has_calls)

Question:

I’m trying to test the order of the sub-functions inside of the main function:

def get_data():
    pass

def process_data(data):
    pass

def notify_admin(action):
    pass

def save_data(data):
    pass


def main_func():
    notify_admin('start')
    data = get_data()
    processed_data = process_data(data)
    save_data(processed_data)
    notify_admin('finish')

I’m using pytest, so far I’ve come up with this:

import pytest

from unittest.mock import patch, Mock, call
from main_func import main_func


@patch('main_func.notify_admin')
@patch('main_func.get_data')
@patch('main_func.process_data')
@patch('main_func.save_data')
def test_main_func(mock_4, mock_3, mock_2, mock_1):
    execution_order = [mock_1, mock_2, mock_3, mock_4]
    order_mock = Mock()
    for order, mock in enumerate(execution_order):
        order_mock.attach_mock(mock, f'f_{order}')

    main_func()
    order_mock.assert_has_calls([
        call.f_1(),
        call.f_2(),
        call.f_3(),
        call.f_4(),
        call.f_1(),
    ])


This is an error, which I'm not sure how to resolve:
E               AssertionError: Calls not found.
E               Expected: [call.f_1(), call.f_2(), call.f_3(), call.f_4(), call.f_1()]
E               Actual: [call.f_1('start'),
E                call.f_2(),
E                call.f_3(<MagicMock name='mock.f_3()' id='2049968460848'>),
E                call.f_4(<MagicMock name='mock.f_2()' id='2049968489424'>),
E                call.f_1('finish')]

Could you please suggest ways to resolve it or maybe implement it in a different way?
I’ve read documentation of assert_has_calls but I’m still not sure how to use it for this particular case.

Asked By: venv

||

Answers:

If you want to check the call order without the argument list, you can use the method_calls attribute of the mock, which contains a list of calls in the order they are made, and only check their name:

    ...      
    main_func()
    assert len(order_mock.method_calls) == 4

    assert order_mock.method_calls[0][0] == "f_1"
    assert order_mock.method_calls[1][0] == "f_2"
    assert order_mock.method_calls[2][0] == "f_3"
    assert order_mock.method_calls[3][0] == "f_4"

Each method call is a tuple of name, positional arguments and keyword arguments, so if you want to check only the name you can just use the first index.

Note that the output of your test does not seem to match this, but this is a matter of your actual application logic.

If you are using has_calls, you have to provide each argument, which is also possible. This time taking the actual result of your test, something like this should work:

    ...
    main_func()
    order_mock.assert_has_calls([
        call.f_1('start'),
        call.f_2(),
        call.f_3(mock1),
        call.f_4(mock2),
        call.f_1('finish')
    ])
Answered By: MrBean Bremen
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.