关于单元测试:基于输入参数模拟python函数

Mocking python function based on input arguments

我们一直在使用Mock for python。

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

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

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

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

即便如此,如果我多次调用foo(),我也可以使用

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

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

这似乎可以在mock上用于python

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

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

有任何想法吗?


If side_effect is a function then whatever that function returns is
what calls to the mock return. The side_effect function is called with
the same arguments as the mock. This allows you to vary the return
value of the call dynamically, based on the input:

1
2
3
4
5
6
7
8
9
10
>>> 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


正如Python Mock对象所指出的那样,多次调用方法

解决方案是编写自己的side_effect

1
2
3
4
5
6
7
8
9
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

这就是诀窍


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

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

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

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

现在:

1
2
3
4
5
6
7
8
9
10
11
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'

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


只是为了表明另一种方式:

1
2
3
4
5
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方法(在测试中),根据提供的参数返回不同的对象:

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

在您的测试功能中,您可以:

1
2
3
4
5
6
7
8
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`


我知道这是一个很老的问题,可能有助于使用python lamdba进行改进

1
2
3
4
5
6
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