根据输入参数模拟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”等于“ something”,那么我想返回“ my_cool_mock”

这似乎可以在python的嘲笑上使用

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

我一直在寻找如何通过Mock实现相同的目标而没有成功?

有任何想法吗?


2
可能是这个答案将帮助- stackoverflow.com/a/7665754/234606
naiquevin

@naiquevin完美解决了队友的问题,谢谢!
Juan Antonio Gomez Moriano

我不知道您可以在Python中使用Mocktio,为此+1!

如果您的项目使用pytest,则可能需要利用pytest monkeypatch。Monkeypatch的目的更多是为了“为了测试而替换此功能”,而Mock是您还要检查mock_calls或对其进行断言等时使用的东西。两者都有一个地方,我经常在给定的测试文件中在不同时间使用两者。
漂流守望者

Answers:


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函数重命名为其他名称吗?(我知道,我知道,这很简单,但是函数名称和参数名称不同的事实提高了可读性:)
Juan Antonio Gomez Moriano

6
我可以使用@JuanAntonioGomezMoriano,但是在这种情况下,我只是直接引用文档,所以如果引用没有被特别打断,我有点讨厌编辑引用。
2013年

而且这些年后都只是学究,但这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
这使我比选择的答案更清楚,因此感谢您回答您自己的问题:)
卡·贝塞拉

14

副作用带有一个函数(也可以是lambda函数),因此对于简单的情况,您可以使用:

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'

希望这会对某人有所帮助!


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

这将返回一个不接受参数的可调用对象(例如Django的timezone.now()),但是我的ock_year函数可以。


感谢您的优雅解决方案。我想补充一点,如果您的原始函数具有其他参数,则需要在生产代码中使用关键字来调用它们,否则这种方法将行不通。您收到错误:got multiple values for argument
Erik Kalkoken

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

如果您“想在输入参数具有特定值时返回固定值”,则可能甚至不需要模拟并且可以使用dictwith及其get方法:

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

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

当您的假货的输出是输入的映射时,这很好用。当它是输入的函数时,我建议side_effect按照Amber的答案使用。

如果要保留Mock的功能(assert_called_oncecall_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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.