Насмешливая функция python на основе входных аргументов

150

Мы использовали Mock для Python некоторое время.

Теперь у нас есть ситуация, в которой мы хотим смоделировать функцию

def foo(self, my_param):
    #do something here, assign something to my_result
    return my_result

Обычно, способ посмеяться над этим будет (при условии, что foo является частью объекта)

self.foo = MagicMock(return_value="mocked!")

Даже если я вызываю foo () пару раз, я могу использовать

self.foo = MagicMock(side_effect=["mocked once", "mocked twice!"])

Теперь я сталкиваюсь с ситуацией, в которой я хочу вернуть фиксированное значение, когда входной параметр имеет конкретное значение. Итак, если, скажем, «my_param» равно «что-то», то я хочу вернуть «my_cool_mock»

Похоже, это доступно на mockito для python

when(dummy).foo("something").thenReturn("my_cool_mock")

Я искал, как добиться того же с Mock без успеха?

Любые идеи?

Хуан Антонио Гомес Мориано
источник
2
Может быть, этот ответ поможет - stackoverflow.com/a/7665754/234606
naiquevin
@naiquevin Это отлично решает проблему, приятель, спасибо!
Хуан Антонио Гомес Мориано
Я понятия не имел, что вы можете использовать Mocktio с Python, +1 за это!
Бен
Если ваш проект использует Pytest, для этой цели вы можете использовать monkeypatch. Monkeypatch больше подходит для «замены этой функции ради тестирования», тогда как Mock - это то, что вы используете, когда вы также хотите проверить mock_callsили сделать утверждения о том, с чем она была вызвана, и так далее. Есть место для обоих, и я часто использую оба в разное время в данном тестовом файле.
Driftcatcher
Возможный дубликат объекта Python Mock с методом, вызываемым несколько раз
Melebius

Ответы:

187

Если side_effectэто функция, то все, что возвращает эта функция, - это то, что вызывает ложное возвращение. side_effectФункция вызывается с теми же аргументами, что и издеваться. Это позволяет динамически изменять возвращаемое значение вызова на основе входных данных:

>>> def side_effect(value):
...     return value + 1
...
>>> m = MagicMock(side_effect=side_effect)
>>> m(1)
2
>>> m(2)
3
>>> m.mock_calls
[call(1), call(2)]

http://www.voidspace.org.uk/python/mock/mock.html#calling

янтарный
источник
25
Чтобы облегчить ответ, не могли бы вы переименовать функцию side_effect в другое? (я знаю, я знаю, это довольно просто, но улучшает читабельность тем, что имя функции и имя параметра различаются :)
Хуан Антонио Гомес Мориано
6
@JuanAntonioGomezMoriano Я мог бы, но в этом случае я просто прямо цитирую документацию, поэтому мне немного не хочется редактировать цитату, если она специально не нарушена.
Январь
и просто быть педантичным все эти годы спустя, но side effectэто точный термин: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh
7
@ Они не жалуются на имя CallableMixin.side_effect, а на то, что отдельная функция, определенная в примере, имеет то же имя.
OrangeDog
48

Как указано в Python Mock объект с методом, вызываемым несколько раз

Решение состоит в том, чтобы написать свой собственный побочный эффект

def my_side_effect(*args, **kwargs):
    if args[0] == 42:
        return "Called with 42"
    elif args[0] == 43:
        return "Called with 43"
    elif kwargs['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect

Это делает трюк

Хуан Антонио Гомес Мориано
источник
2
Это сделало его более понятным для меня, чем выбранный ответ, так что спасибо за ответ на свой вопрос :)
Luca Bezerra
15

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

m = MagicMock(side_effect=(lambda x: x+1))
Шубхам Чаудхари
источник
4

Я закончил здесь поиском « как смоделировать функцию на основе входных аргументов » и наконец решил эту проблему, создав простую вспомогательную функцию:

def mock_responses(responses, default_response=None):
  return lambda input: responses[input] if input in responses else default_response

Сейчас:

my_mock.foo.side_effect = mock_responses(
  {
    'x': 42, 
    'y': [1,2,3]
  })
my_mock.goo.side_effect = mock_responses(
  {
    'hello': 'world'
  }, 
  default_response='hi')
...

my_mock.foo('x') # => 42
my_mock.foo('y') # => [1,2,3]
my_mock.foo('unknown') # => None

my_mock.goo('hello') # => 'world'
my_mock.goo('ey') # => 'hi'

Надеюсь, это кому-нибудь поможет!

Manu
источник
2

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

def mock_year(year):
    return datetime.datetime(year, 11, 28, tzinfo=timezone.utc)
@patch('django.utils.timezone.now', side_effect=partial(mock_year, year=2020))

Это вернет вызываемый объект, который не принимает параметры (как timezone.now () Django), но моя функция mock_year принимает.

Hernan
источник
Спасибо за это элегантное решение. Я хотел бы добавить, что если ваша исходная функция имеет дополнительные параметры, их необходимо вызывать с ключевыми словами в вашем рабочем коде, иначе этот подход не будет работать. Вы получаете ошибку: got multiple values for argument.
Эрик Калкокен
1

Просто чтобы показать другой способ сделать это:

def mock_isdir(path):
    return path in ['/var/log', '/var/log/apache2', '/var/log/tomcat']

with mock.patch('os.path.isdir') as os_path_isdir:
    os_path_isdir.side_effect = mock_isdir
Калеб
источник
1

Вы также можете использовать @mock.patch.object:

Скажем, модуль my_module.pyиспользует pandasдля чтения из базы данных, и мы хотели бы протестировать этот модуль методом насмешки pd.read_sql_table(который принимает в table_nameкачестве аргумента).

Что вы можете сделать, это создать (внутри вашего теста) db_mockметод, который возвращает разные объекты в зависимости от предоставленного аргумента:

def db_mock(**kwargs):
    if kwargs['table_name'] == 'table_1':
        # return some DataFrame
    elif kwargs['table_name'] == 'table_2':
        # return some other DataFrame

В своей тестовой функции вы затем делаете:

import my_module as my_module_imported

@mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
def test_my_module(mock_read_sql_table):
    # You can now test any methods from `my_module`, e.g. `foo` and any call this 
    # method does to `read_sql_table` will be mocked by `db_mock`, e.g.
    ret = my_module_imported.foo(table_name='table_1')
    # `ret` is some DataFrame returned by `db_mock`
Томаш Бартковяк
источник
1

Если вы «хотите вернуть фиксированное значение, когда входной параметр имеет определенное значение» , возможно, вам даже не нужен макет, и вы могли бы использовать метод dictнаряду с его getметодом:

foo = {'input1': 'value1', 'input2': 'value2'}.get

foo('input1')  # value1
foo('input2')  # value2

Это хорошо работает, когда вывод вашей подделки является отображением ввода. Когда это функция ввода, я бы предложил использовать side_effectв соответствии с ответом Амбер .

Вы также можете использовать комбинацию обоих, если хотите сохранить Mockвозможности ( assert_called_onceи call_countт. Д.):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
Арсений Панфилов
источник
Это очень умно.
Эмиллер
-6

Я знаю, это довольно старый вопрос, может помочь как улучшение с использованием Python lamdba

self.some_service.foo.side_effect = lambda *args:"Called with 42" \
            if args[0] == 42 \
            else "Called with 42" if args[0] == 43 \
            else "Called with 43" if args[0] == 43 \
            else "Called with 45" if args[0] == 45 \
            else "Called with 49" if args[0] == 49 else None
послушник
источник