方法被多次调用的Python Mock对象

ocebsuys  于 2023-02-11  发布在  Python
关注(0)|答案(5)|浏览(142)

我正在测试一个类,它依赖于另一个类(它的一个示例被传递给CUT的init方法),我想用Python Mock库模拟这个类。
我得到的是这样的:

mockobj = Mock(spec=MyDependencyClass)
mockobj.methodfromdepclass.return_value = "the value I want the mock to return"
assertTrue(mockobj.methodfromdepclass(42), "the value I want the mock to return")

cutobj = ClassUnderTest(mockobj)

这很好,但是“methodfromdepclass”是一个参数化的方法,因此我想创建一个单独的模拟对象,根据传递给methodfromdepclass的参数,它返回不同的值。
我想要这种参数化行为的原因是我想创建多个包含不同值的ClassUnderTest示例(这些值由mockobj返回的值产生)。
我在想什么(这当然行不通):

mockobj = Mock(spec=MyDependencyClass)
mockobj.methodfromdepclass.ifcalledwith(42).return_value = "you called me with arg 42"
mockobj.methodfromdepclass.ifcalledwith(99).return_value = "you called me with arg 99"

assertTrue(mockobj.methodfromdepclass(42), "you called me with arg 42")
assertTrue(mockobj.methodfromdepclass(99), "you called me with arg 99")

cutinst1 = ClassUnderTest(mockobj, 42)
cutinst2 = ClassUnderTest(mockobj, 99)

# now cutinst1 & cutinst2 contain different values

如何实现这种“ifcalledwith”语义?

cbjzeqam

cbjzeqam1#

尝试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
vshtjzan

vshtjzan2#

稍微甜一点:

mockobj.method.side_effect = lambda x: {123: 100, 234: 10000}[x]

或对于多个参数:

mockobj.method.side_effect = lambda *x: {(123, 234): 100, (234, 345): 10000}[x]

或使用默认值:

mockobj.method.side_effect = lambda x: {123: 100, 234: 10000}.get(x, 20000)

或两者的组合:

mockobj.method.side_effect = lambda *x: {(123, 234): 100, (234, 345): 10000}.get(x, 20000)

我们快乐地向高处走去。

bxpogfeg

bxpogfeg3#

我在做自己的测试时遇到过这种情况,如果你不关心捕捉对methodfromdepclass()的调用,而只是需要它返回一些东西,那么下面的代码就足够了:

def makeFakeMethod(mapping={}):
    def fakeMethod(inputParam):
        return mapping[inputParam] if inputParam in mapping else MagicMock()
    return fakeMethod

mapping = {42:"Called with 42", 59:"Called with 59"}
mockobj.methodfromdepclass = makeFakeMethod(mapping)

下面是一个参数化的版本:

def makeFakeMethod():
    def fakeMethod(param):
        return "Called with " + str(param)
    return fakeMethod
wswtfjt7

wswtfjt74#

here一样,除了在unittest.mock.mock中使用side_effect之外,还可以将@mock.patch.objectnew_callable一起使用,这样就可以用mock对象修补对象的属性。
假设一个模块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`
stszievb

stszievb5#

您可以使用副作用+ Dict调度模式获得更干净的代码:
例如(使用异步,忽略非异步情况下的未来对象)

@pytest.fixture()
def mock_fixture(mocker):

    # dict to switch results from diferent results
    target_function_args_x_results = {
        'foo': 'result_1',
        'bar': 'result_2',
        'xyz': 'result_3',
    }

    def function_mock_side_effect(*args, **kwargs):
        future = asyncio.Future()
        argument_received = args[0] # ex: ... my_function('foo')
        mock_result = target_function_args_x_results.get(argument_received, {}) # Dict Dispatch Pattern
        future.set_result(mock_result)

        return future

    mocker.patch("src.app.my_module.my_target_function",
                 side_effect=function_mock_side_effect)

相关问题