多次调用方法的 Python Mock 对象

发布于 2024-12-08 00:28:29 字数 1108 浏览 0 评论 0原文

我正在测试一个类,它具有另一个类的依赖项(该类的实例被传递给 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 = 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”类型的语义?

I have a class that I'm testing which has as a dependency another class (an instance of which gets passed to the CUT's init method). I want to mock out this class using the Python Mock library.

What I have is something like:

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)

Which is fine, but "methodfromdepclass" is a parameterized method, and as such I want to create a single mock object where depending on what arguments are passed to methodfromdepclass it returns different values.

The reason I want this parameterized behaviour is I want to create multiple instances of ClassUnderTest that contain different values (the values of which are produced by what gets returned from the mockobj).

Kinda what I'm thinking (this of course does not work):

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

How do I achieve this "ifcalledwith" kind of semantics?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

尘世孤行 2024-12-15 00:28:29

尝试副作用

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

Try 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
趁年轻赶紧闹 2024-12-15 00:28:29

更甜蜜一点:

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)

我们愉快地前进。

A little sweeter:

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

or for multiple arguments:

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

or with a default value:

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

or a combination of both:

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

and merrily on high we go.

浪荡不羁 2024-12-15 00:28:29

我在进行自己的测试时遇到了这个问题。如果您不关心捕获对 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

I've ran into this when I was doing my own testing. If you don't care about capturing calls to your methodfromdepclass() but just need it to return something, then the following may suffice:

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)

Here's a parameterized version:

def makeFakeMethod():
    def fakeMethod(param):
        return "Called with " + str(param)
    return fakeMethod
无畏 2024-12-15 00:28:29

此处所示,除了在 unittest.mock.Mock 你也可以使用 @mock.patch.objectnew_callable,它允许您使用模拟对象修补对象的属性。

假设一个模块 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`

As in here, apart from using side_effect in unittest.mock.Mock you can also use @mock.patch.object with new_callable, which allows you to patch an attribute of an object with a mock object.

Let's say a module my_module.py uses pandas to read from a database and we would like to test this module by mocking pd.read_sql_table method (which takes table_name as argument).

What you can do is to create (inside your test) a db_mock method that returns different objects depending on the argument provided:

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

In your test function you then do:

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`
谁对谁错谁最难过 2024-12-15 00:28:29

您可以使用 Side Effect + Dict Dispatch Pattern 来获得更干净的代码:

例如(使用异步,忽略非异步情况下的未来对象)


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

You can use Side Effect + Dict Dispatch Pattern for a more clean code:

For example (using async, ignore future objects for non async cases)


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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文