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

问题描述 投票:0回答:4

我想要一个单元测试来断言函数中的变量

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
,并获取其输入参数并断言它们具有特定值。

>这不是模拟的工作原理吗?模拟文档的不一致、不完整的示例以及大量与我想做的事情无关的示例让我感到沮丧。

python unit-testing mocking
4个回答
85
投票

您也可以使用

call_args
call_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'])

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

21
投票

您可以使用补丁装饰器,然后调用

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

10
投票

如果你想直接访问参数,这样怎么样?虽然有点多余... 请参阅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, {})

1
投票

上面的答案很有帮助,但我想要一种简单的方法来编写单元测试,当测试的代码更改了模拟函数调用的更改方式而无需进行任何功能更改时,不需要重构。

例如,如果我选择通过关键字部分或完全调用该函数(或构建一个 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 module.function_to_patch has already been replaced by mock,
        # leading to error from _validate_func_signature. If this is your
        # intended usage, don't use assert_called_with()
        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)
© www.soinside.com 2019 - 2024. All rights reserved.