Проверка порядка звонков в нескольких макетах

У меня есть три функции, порядок вызова которых я пытаюсь проверить.

Скажем, в модуле module.py у меня есть следующее

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

Я хочу проверить, что b вызывается после a и до c. Так что получить макет для каждого из a, b и c легко:

# 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

Проверить, что каждый из отдельных имитаций вызван, тоже несложно. Как мне проверить порядок вызовов относительно друг друга?

call_args_list не будет работать, так как поддерживается отдельно для каждого макета.

Я пробовал использовать побочный эффект для регистрации каждого из вызовов:

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

Но это дает мне только аргументы, с которыми были вызваны макеты, но не фактический макет, против которого был сделан вызов. Могу добавить немного логики:

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

И это, кажется, делает работу ... Но есть ли способ лучше? Похоже, что в API уже должно быть что-то, что может сделать это, что мне не хватает.


person Shaun O'Keefe    schedule 27.03.2014    source источник


Ответы (3)


Определите Mock менеджер и прикрепите к нему макеты через attach_mock(). Затем проверьте 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

Просто чтобы проверить, что это работает, измените порядок вызовов функций в main_routine() function add, чтобы увидеть, что он выдает AssertionError.

Дополнительные примеры см. На странице Порядок отслеживания звонков и менее подробные утверждения звонков

Надеюсь, это поможет.

person alecxe    schedule 27.03.2014
comment
Одно важное замечание - не устанавливайте autospec = True внутри patch (...). Если вы установите autospec = True, attach_mock не будет работать правильно. В вашем примере нет автоспецификации, но она часто существует в реальных тестовых случаях. - person Andrey Belyak; 14.04.2016
comment
Этот ответ соответствует коду вопроса, но его трудно читать, потому что нет описательных имен. Я добавляю ответ ниже, чтобы прояснить, как работает attach_mocks. - person lortimer; 31.01.2019
comment
Комментарий @AndreyBelyak сейчас устарел - это была ошибка, и с тех пор она исправлена ​​(см. здесь) - person wim; 31.08.2020

Мне нужен этот ответ сегодня, но код примера в вопросе действительно трудно читать, потому что аргументы вызова совпадают с именами имитаторов в диспетчере и в рамках теста. Вот официальная документация по этой концепции, а ниже - более наглядный пример для не роботов. Все модули, которые я исправляю, созданы для примера:

@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

Обратите внимание, что в этом случае вы должны использовать attach_mock, потому что ваши макеты были созданы patch. Моки с именами, в том числе созданные patch, должны быть прикреплены через attach_mock, чтобы этот код работал. Вам не нужно использовать attach_mock, если вы создаете свои собственные Mock объекты без имен:

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

Если вы хотите, чтобы при отсутствии порядка или ожидаемых вызовов выводилось сообщение о сбое четкого утверждения, используйте вместо этого следующую строку утверждения.

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)
])
person lortimer    schedule 31.01.2019

Более чистым решением было бы обернуть ваши функции в класс, а затем имитировать класс в тесте. Это избавит от необходимости делать какие-либо исправления (всегда плюс).

# module.py

class Wrapper:
    def a(self, *args):
        pass

    def b(self, *args):
        pass

    def c(self, *args):
        pass

    def main_routine(self):
        a_args = ('arg for a',)
        b_args = ('arg for b',)
        c_args = ('arg for c',)

        self.a(*a_args)
        self.b(*b_args)
        self.c(*c_args)

В тестовом файле вы создаете фиктивный класс-оболочку, а затем вставляете фиктивную оболочку в качестве аргумента self при вызове Wrapper.main_method (обратите внимание, что это не создает экземпляр класса).

# module_test.py

from unittest.mock import MagicMock, call

from module import Wrapper


def test_main_routine():
    mock_wrapper = MagicMock()
    Wrapper.main_routine(mock_wrapper)
    expected_calls = [call.a('arg for a'),
                      call.b('arg for b'),
                      call.c('arg for c')]
    mock_wrapper.assert_has_calls(expected_calls)

Преимущества:

  • Исправление не требуется
  • В тесте вам нужно всего лишь один раз (а не 2-3 раза) ввести имя вызываемого метода.
  • Использует assert_has_calls вместо сравнения атрибута mock_calls со списком вызовов.
  • Может быть преобразован в общую check_for_calls функцию (см. Ниже)
# module_better_test.py

from unittest.mock import MagicMock, call

from module import Wrapper


def test_main_routine():
    expected_calls = [call.a('arg for a'),
                      call.b('arg for b'),
                      call.c('arg for c')]
    check_for_calls('main_routine', expected_calls)


def check_for_calls(method, expected_calls):
    mock_wrapper = MagicMock()
    getattr(Wrapper, method)(mock_wrapper)
    mock_wrapper.assert_has_calls(expected_calls)

person Chris Collett    schedule 27.05.2021