Python 模拟 call_args_list 解包元组以对参数进行断言

Posted

技术标签:

【中文标题】Python 模拟 call_args_list 解包元组以对参数进行断言【英文标题】:Python mock call_args_list unpacking tuples for assertion on arguments 【发布时间】:2017-02-01 19:57:32 【问题描述】:

我在处理 Mock.call_args_list 返回的嵌套元组时遇到了一些麻烦。

def test_foo(self):
    def foo(fn):
        fn('PASS and some other stuff')

    f = Mock()
    foo(f)
    foo(f)
    foo(f)

    for call in f.call_args_list:
        for args in call:
            for arg in args:
                self.assertTrue(arg.startswith('PASS'))

我想知道是否有更好的方法来解压缩模拟对象上的 call_args_list 以便做出我的断言。这个循环有效,但感觉必须有更直接的方法。

【问题讨论】:

我认为这真的取决于你想要做出什么样的断言。您是否尝试仅检查位置参数?您是否要检查位置和关键字参数?您是要检查关键字本身还是通过关键字参数传递的值?例如如果您只想检查第一个位置参数是否以 'PASS' 开头,那么 self.assertTrue(call[0][0].startswith('Pass')) 应该可以在没有内部 2 个循环的情况下完成操作。 【参考方案1】:

更好的方法可能是建立自己的预期调用,然后使用直接断言:

>>> from mock import call, Mock
>>> f = Mock()
>>> f('first call')
<Mock name='mock()' id='31270416'>
>>> f('second call')
<Mock name='mock()' id='31270416'>
>>> expected_calls = [call(s + ' call') for s in ('first', 'second')]
>>> f.assert_has_calls(expected_calls)

请注意,调用必须是顺序的,如果您不希望这样,则将 any_order kwarg 覆盖到断言中。

另外请注意,在调用之前或之后允许有额外的调用 指定的调用。如果你不想这样,你需要添加另一个断言:

>>> assert f.call_count == len(expected_calls)

解决 mgilson 的评论,这里是创建一个虚拟对象的示例,您可以将其用于通配符相等性比较:

>>> class AnySuffix(object):
...     def __eq__(self, other):
...         try:
...             return other.startswith('PASS')
...         except Exception:
...             return False
...        
>>> f = Mock()
>>> f('PASS and some other stuff')
<Mock name='mock()' id='28717456'>
>>> f('PASS more stuff')
<Mock name='mock()' id='28717456'>
>>> f("PASS blah blah don't care")
<Mock name='mock()' id='28717456'>
>>> expected_calls = [call(AnySuffix())]*3
>>> f.assert_has_calls(expected_calls)

以及故障模式的一个例子:

>>> Mock().assert_has_calls(expected_calls)
AssertionError: Calls not found.
Expected: [call(<__main__.AnySuffix object at 0x1f6d750>),
 call(<__main__.AnySuffix object at 0x1f6d750>),
 call(<__main__.AnySuffix object at 0x1f6d750>)]
Actual: []

【讨论】:

这只有在 OP 真正知道每个调用应该是什么样子确切时才有效。基于这个问题,看起来 OP 只知道每个参数的第一部分应该是什么样子(这很奇怪......但是嘿......) 嗯,call 只是一个元组,所以仍然可以修改它来工作。我将编辑以显示如何... 更多的是我不希望我的测试关心参数第一部分之后的内容,因为后面的内容非常冗长并且变化很大,所以让测试断言它变成真的很乏味。 我添加了一个示例来展示如何模糊匹配呼叫。然而,作为一个警示故事,如果你不能准确预测测试中调用的结果,这通常是一个警告信号,表明有些东西本应该是不被嘲笑的......【参考方案2】:

我认为这里的许多困难都包含在“调用”对象的处理上。它可以被认为是一个有 2 个成员 (args, kwargs) 的元组,因此打开它通常很不错:

args, kwargs = call

解压后,您可以分别对 args 和 kwargs 进行断言(因为一个是元组,另一个是 dict)

def test_foo(self):
    def foo(fn):
        fn('PASS and some other stuff')

    f = Mock()
    foo(f)
    foo(f)
    foo(f)

    for call in f.call_args_list:
        args, kwargs = call
        self.assertTrue(all(a.startswith('PASS') for a in args))

请注意,有时简洁并没有帮助(例如,如果有错误):

for call in f.call_args_list:
    args, kwargs = call
    for a in args:
        self.assertTrue(a.startswith('PASS'), msg="%s doesn't start with PASS" % a)

【讨论】:

@wim -- 是的。我已经看到你的方式使用了很多次,但总觉得尝试成为一个记录/播放类型的框架太过分了。 用于 python 单元测试的记录/播放框架,但我从来没有真正认为unittest.mock 是其中之一:-) 哦,args, kwargs = call 是所有 call_args_list 黑客攻击的线索。谢谢! 如果您知道只有一个位置参数(在这种情况下为message),您可以一步完成:(message, ), kwargs = call

以上是关于Python 模拟 call_args_list 解包元组以对参数进行断言的主要内容,如果未能解决你的问题,请参考以下文章

python如何模拟点击这样的网址链接?

新手,python怎么模拟网页按钮点击

新手,python怎么模拟网页按钮点击

如何用python模拟点击onclick

[python3.5][PyUserInput]模拟鼠标和键盘模拟

Python模拟中的模拟属性?