Python单元测试模拟,获取模拟函数的输入参数

cqoc49vn  于 2023-09-29  发布在  Python
关注(0)|答案(4)|浏览(121)

我想要一个单元测试来Assert一个函数中的变量action被设置为它的预期值,这个变量被使用的唯一时间是当它被传递到一个库的调用中时。

Class Monolith(object):
    def foo(self, raw_event):
        action =  # ... Parse Event
        # Middle of function
        lib.event.Event(METADATA, action)
        # Continue on to use the build event.

我的想法是,我可以模拟lib.event.Event,并获得其输入参数,并Assert它们具有特定的值。
”这不是嘲笑的工作吗?模拟文档的不一致性、半示例性和过多的示例与我想要做的事情无关,让我感到沮丧。

ajsxfq5m

ajsxfq5m1#

您也可以使用call_argscall_args_list
一个简单的例子如下所示:

import mock
import unittest

class TestExample(unittest.TestCase):

    @mock.patch('lib.event.Event')
    def test_example1(self, event_mocked):
        args, kwargs = event_mocked.call_args
        args = event_mocked.call_args.args  # alternatively 
        self.assertEqual(args, ['metadata_example', 'action_example'])

我只是快速地为可能需要它的人写了这个例子-我还没有实际测试过这个,所以可能有小错误。

yv5phkfx

yv5phkfx2#

你可以使用补丁装饰器,然后像这样调用assert_called_with到模拟的对象:
如果你有这样的结构:

example.py
tests.py
lib/__init__.py
lib/event.py

example.py的内容是:

import lib

METADATA = 'metadata_example'

class Monolith(object):

    def foo(self, raw_event):
        action =  'action_example' # ... Parse Event
        # Middle of function
        lib.event.Event(METADATA, action)
        # Continue on to use the build event.

lib/event.py的含量为:

class Event(object):

    def __init__(self, metadata, action):
        pass

tests.py的代码应该是这样的:

import mock
import unittest

from lib.event import Event
from example import Monolith

class TestExample(unittest.TestCase):

    @mock.patch('lib.event.Event')
    def test_example1(self, event_mocked):
        # Setup
        m = Monolith()

        # Exercise
        m.foo('raw_event')

        # Verify
        event_mocked.assert_called_with('metadata_example', 'action_example')
eh57zj3b

eh57zj3b3#

如果你想直接访问参数,这个怎么样?虽然有点多余请访问https://docs.python.org/3.6/library/unittest.mock.html#unittest.mock.call.call_list

import mock
import unittest

from lib.event import Event
from example import Monolith

class TestExample(unittest.TestCase):

    @mock.patch('lib.event.Event')
    def test_example1(self, event_mocked):
        # Setup
        m = Monolith()

        # Exercise
        m.foo('raw_event')

        # Verify
        name, args, kwargs = m.mock_calls[0]
        self.assertEquals(name, "foo")
        self.assertEquals(args, ['metadata_example', 'action_example'])
        self.assertEquals(kwargs, {})
mutmk8jj

mutmk8jj4#

上面的答案很有帮助,但我想要一种简单的方法来编写一个单元测试,当测试代码改变了模拟函数调用的方式而没有任何功能更改时,不需要重构。
例如,如果我选择部分或完全通过关键字调用函数(或构建一个kwargs字典并插入),而不更改传入的值:

def function_being_mocked(x, y):
  pass

# Initial code
def function_being_tested():
  # other stuff
  function_being_mocked(42, 150)

# After refactor
def function_being_tested():
  # other stuff
  function_being_mocked(x=42, y=150)
  # or say kwargs = {'x': 42, 'y': 150} and function_being_mocked(**kwargs)

这可能有点过分,但我希望我的单元测试不必担心函数调用格式的更改,只要函数调用中包含预期的值(甚至包括指定或不指定默认值)。
这是我想出的解决办法。我希望这有助于简化您的测试体验:

from inspect import Parameter, Signature, signature

class DefaultValue(object):
    def __init__(self, value):
        self.value = value

    def __eq__(self, other_value) -> bool:
        if isinstance(other_value, DefaultValue):
            return self.value == other_value.value
        return self.value == other_value

    def __repr__(self) -> str:
        return f'<DEFAULT_VALUE: {self.value}>'

def standardize_func_args(func_sig, args, kwargs, is_method):
    kwargs = kwargs.copy()

    # Remove self/cls from kwargs if is_method=True
    parameters = list(func_sig.parameters.values())
    if is_method:
        parameters = list(parameters)[1:]

    # Positional arguments passed in need to line up index-wise
    # with the function signature.
    for (i, arg_value) in enumerate(args):
        kwargs[parameters[i].name] = arg_value

    kwargs.update({
        param.name: DefaultValue(param.default)
        for param in parameters
        if param.name not in kwargs
    })

    # Order the resulting kwargs by the function signature parameter order
    # so that the stringification in assert error message is consistent on
    # the objects being compared.
    return {
        param.name: kwargs[param.name]
        for param in parameters
    }

def _validate_func_signature(func_sig: Signature):
    assert not any(
        p.kind == Parameter.VAR_KEYWORD or p.kind == Parameter.VAR_POSITIONAL
        for p in func_sig.parameters.values()
    ), 'Functions with *args or **kwargs not supported'

def __assert_called_with(mock, func, is_method, *args, **kwargs):
    func_sig = signature(func)
    _validate_func_signature(func_sig)

    mock_args = standardize_func_args(
        func_sig, mock.call_args.args, mock.call_args.kwargs, is_method)
    func_args = standardize_func_args(func_sig, args, kwargs, is_method)

    assert mock_args == func_args, f'Expected {func_args} but got {mock_args}'

def assert_called_with(mock, func, *args, **kwargs):
    __assert_called_with(mock, func, False, *args, **kwargs)

def assert_method_called_with(mock, func, *args, **kwargs):
    __assert_called_with(mock, func, True, *args, **kwargs)

使用方法:

from unittest.mock import MagicMock

def bar(x, y=5, z=25):
    pass

mock = MagicMock()
mock(42)

assert_called_with(mock, bar, 42) # passes
assert_called_with(mock, bar, 42, 5) # passes
assert_called_with(mock, bar, x=42) # passes
assert_called_with(mock, bar, 42, z=25) # passes
assert_called_with(mock, bar, z=25, x=42, y=5) # passes

# AssertionError: Expected {'x': 51, 'y': <DEFAULT_VALUE: 5>, 'z': <DEFAULT_VALUE: 25>} but got {'x': 42, 'y': <DEFAULT_VALUE: 5>, 'z': <DEFAULT_VALUE: 25>}
assert_called_with(mock, bar, 51)

# AssertionError: Expected {'x': 42, 'y': 51, 'z': <DEFAULT_VALUE: 25>} but got {'x': 42, 'y': <DEFAULT_VALUE: 5>, 'z': <DEFAULT_VALUE: 25>}
assert_called_with(mock, bar, 42, 51)

请注意使用前的警告。assert_called_with()需要引用原始函数。如果你在单元测试中使用装饰器@unittest.mock.patch,它可能会适得其反,因为你试图查找函数签名可能会得到模拟对象而不是原始函数:

from unittest import mock

class Tester(unittest.TestCase):
    @unittest.mock.patch('module.function_to_patch')
    def test_function(self, mock):
        function_to_be_tested()
        # Here mock == module.function_to_patch
        assert_called_with(mock, module.function_to_patch, *args, **kwargs)

我建议使用unittest.mock.patch.object,它要求你导入正在打补丁的函数,因为我的代码无论如何都需要引用函数:

class Tester(unittest.TestCase):
    def test_function(self):
        orig_func_patched = module.function_to_patch
        with unittest.mock.patch.object(module, 'function_to_patch') as mock:
            function_to_be_tested()
        
            assert_called_with(mock, orig_func_patched, *args, **kwargs)

相关问题