基于输入参数模拟python函数

Jua*_*ano 126 python unit-testing mocking mockito

我们一直在使用Mock for python.

现在,我们有一种情况,我们想要模拟一个函数

def foo(self, my_param):
    #do something here, assign something to my_result
    return my_result
Run Code Online (Sandbox Code Playgroud)

通常,模拟这个的方法是(假设foo是对象的一部分)

self.foo = MagicMock(return_value="mocked!")
Run Code Online (Sandbox Code Playgroud)

甚至,如果我打电话给foo()几次我可以使用

self.foo = MagicMock(side_effect=["mocked once", "mocked twice!"])
Run Code Online (Sandbox Code Playgroud)

现在,我面临的情况是,当输入参数具有特定值时,我想返回固定值.所以如果让我们说"my_param"等于"某事"那么我想要返回"my_cool_mock"

这似乎可以在mock上用于python

when(dummy).foo("something").thenReturn("my_cool_mock")
Run Code Online (Sandbox Code Playgroud)

我一直在寻找如何与Mock达成同样的目标并没有成功?

有任何想法吗?

Amb*_*ber 159

如果side_effect是一个函数,那么该函数返回的是调用mock返回的函数.使用side_effect与mock相同的参数调用该函数.这允许您根据输入动态地改变调用的返回值:

>>> 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)]
Run Code Online (Sandbox Code Playgroud)

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

  • 只是为了使答案更容易,你能否将side_effect函数重命名为其他东西?(我知道,我知道,这很简单,但提高了可读性,因为函数名称和参数名称不同:) (16认同)
  • @JuanAntonioGomezMoriano我可以,但在这种情况下,我只是直接引用文档,所以我有点厌恶编辑引用,如果它没有特别打破. (5认同)
  • @Ish他们并没有抱怨CallableMixin.side_effect的名称,但是示例中定义的单独函数具有相同的名称。 (5认同)

Jua*_*ano 35

正如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 kwarg['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect
Run Code Online (Sandbox Code Playgroud)

这就是诀窍

  • 这使我比选择的答案更清楚,因此感谢您回答自己的问题:) (2认同)

Shu*_*ary 12

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

m = MagicMock(side_effect=(lambda x: x+1))
Run Code Online (Sandbox Code Playgroud)


Man*_*anu 7

我最终在这里寻找“如何根据输入参数模拟函数”,我终于解决了这个问题,创建了一个简单的辅助函数:

def mock_responses(responses, default_response=None):
  return lambda input: responses[input] if input in responses else default_response
Run Code Online (Sandbox Code Playgroud)

现在:

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'
Run Code Online (Sandbox Code Playgroud)

希望这会帮助某人!


Ars*_*lov 6

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

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

foo('input1')  # value1
foo('input2')  # value2
Run Code Online (Sandbox Code Playgroud)

当你的假的输出是输入的映射时,这很有效。当它是输入函数时,我建议side_effect按照Amber的回答使用。

如果您想保留Mock的功能(等)assert_called_once,您也可以使用两者的组合call_count

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
Run Code Online (Sandbox Code Playgroud)

  • 这是非常聪明的。 (3认同)

小智 5

如果您想使用带参数的函数但您模拟的函数不带参数,您也可以使用partialfrom functools。例如像这样:

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

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


小智 5

虽然side_effect可以达到目的,但是side_effect为每个测试用例设置函数并不是那么方便。

我写了一个轻量级的Mock(叫做 NextMock)来增强内置的 Mock 来解决这个问题,这里是一个简单的例子:

from nextmock import Mock

m = Mock()

m.with_args(1, 2, 3).returns(123)

assert m(1, 2, 3) == 123
assert m(3, 2, 1) != 123
Run Code Online (Sandbox Code Playgroud)

它还支持参数匹配器:

from nextmock import Arg, Mock

m = Mock()

m.with_args(1, 2, Arg.Any).returns(123)

assert m(1, 2, 1) == 123
assert m(1, 2, "123") == 123
Run Code Online (Sandbox Code Playgroud)

希望这个包能让测试更愉快。随时提供任何反馈。