基于输入参数模拟python函数

我们为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的Mockito上可用

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

我一直在寻找如何实现同样的Mock没有成功?

什么好主意吗?

190780 次浏览
如果side_effect_func是一个函数,则该函数返回的值为 什么调用模拟返回。side_effect_func函数被调用 和模拟的论点一样。这允许你改变回报 基于输入动态调用的值:

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

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

Python模拟对象,方法被多次调用所示

解决办法是写我自己的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

这就成功了

Side effect接受一个函数(也可以是lambda函数),所以对于简单的情况,你可以使用:

m = MagicMock(side_effect=(lambda x: x+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

你也可以使用@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`

我最终在这里寻找“如何模拟一个基于输入参数的函数”,我最终解决了这个问题,创建了一个简单的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'

希望这能帮助到一些人!

如果你想使用一个带参数的函数,而你要模拟的函数不带参数,你也可以使用partial from 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()),但我的mock_year函数可以。

如果你"当输入参数有特定值时,想返回一个固定值",也许你甚至不需要一个mock,并且可以使用dict及其get方法:

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


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

当你的fake的输出是输入的映射时,这工作得很好。当它是输入的函数时,我建议根据琥珀色的的答案使用side_effect

如果你想保留Mock的功能(assert_called_oncecall_count等),你也可以使用两者的组合:

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

虽然side_effect可以实现这个目标,但是为每个测试用例设置side_effect函数并不方便。

我写了一个轻量级的模拟(称为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

它还支持参数匹配器:

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

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