Знущання з функції пітона на основі вхідних аргументів


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

Відповіді:


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
@Ish вони не скаржаться на ім'я CallableMixin.side_effect, але що окрема функція, визначена в прикладі, має те саме ім'я.
OrangeDog

48

Як зазначено в об'єкті Python Mock з методом, який називається кілька разів

Рішення - написати власний side_effect

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
Це зробило для мене зрозуміліше обрану відповідь, тож дякую за відповідь на власне запитання :)
Лука Безерра

15

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

m = MagicMock(side_effect=(lambda x: x+1))

4

Я нарешті шукаю " як знущатися над функцією на основі вхідних аргументів ", і я нарешті вирішив це, створивши просту функцію aux:

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'

Сподіваюся, що це комусь допоможе!


2

Крім того, можна використовувати partialз , 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))

Це поверне дзвінок, який не приймає параметри (як timejan.now () Django), але моя mock_year функція.


Дякую за це елегантне рішення. Мені подобається додати, що якщо у вашої оригінальної функції є додаткові параметри, їх потрібно викликати за допомогою ключових слів у виробничому коді, інакше цей підхід не працюватиме. Ви отримуєте помилку: 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«S можливості ( assert_called_once, і call_countт.д.):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get

Це дуже розумно.
emyller

-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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.