Python的assert_called_with,是否有通配符?

47
假设我有一个在Python中设置如下的类。
from somewhere import sendmail

class MyClass:

    def __init__(self, **kargs):
        self.sendmail = kwargs.get("sendmail", sendmail)  #if we can't find it, use imported def

    def publish():

        #lots of irrelevant code
        #and then

        self.sendmail(mail_to, mail_from, subject, body, format= 'html')

正如您所见,我已经为self.sendmail函数提供了参数化选项。

现在,在测试文件中。

Class Tester():

    kwargs = {"sendmail": MagicMock(mail_from= None, mail_to= None, subject= None, body= None, format= None)}
    self.myclass = MyClass(**kwargs)

    ##later on
    def testDefaultEmailHeader():

        default_subject = "Hello World"
        self.myclass.publish()

        self.myclass.sendmail.assert_called()  #this is doing just fine
        self.myclass.sendmail.assert_called_with(default_subject)  #this is having issues

由于某些原因,我收到了错误信息。
AssertionError: Expected call: mock('Hello World')
                Actual Call : mock('defaultmt', 'defaultmf', 'Hello World', 'default_body', format= 'html')

基本上,assert期望sendmail只使用一个变量进行调用,但实际上却使用了全部5个变量。问题在于,我并不关心其他4个变量是什么!我只想确保它被正确地调用了主题。

我尝试使用模拟占位符ANY,但结果相同。

self.myclass.sendmail.assert_called_with(ANY, ANY, 'Hello World', ANY, ANY)

AssertionError: Expected call: mock(<ANY>, <ANY>, 'Hello World', <ANY>, <ANY>)
Actual Call : mock('defaultmt', 'defaultmf', 'Hello World', 'default_body, 'format= 'html') 

我不确定如何处理这个问题。如果我们只关心其中一个变量并希望忽略其他变量,有没有人有建议?


1
你是不是想用 format='html' 而不是 'format='html',后者含义不明确?试试这个:self.myclass.sendmail.assert_called_with(ANY, ANY, 'Hello World', ANY, format=ANY) - alko
3个回答

51

如果您使用命名参数subject调用sendmail,最好检查命名参数是否与您期望的匹配:

args, kwargs = self.myclass.sendmail.call_args
self.assertEqual(kwargs['subject'], "Hello World")

这假定 sendmail 的两个实现都有一个名为 subject 的命名参数。如果不是这种情况,您可以使用位置参数来实现相同的效果:

这假定 sendmail 的两个实现都有一个名为 subject 的命名参数。如果不是这种情况,您可以使用位置参数来实现相同的效果:

args, kwargs = self.myclass.sendmail.call_args
self.assertTrue("Hello World" in args)

您可以明确指定参数的位置(即传递给sendmail的第一个参数或第三个参数),但这取决于正在测试的sendmail实现。


1
在第一个示例中,您的MagicMock有五个命名参数,而您只传递了一个位置参数,因此它不知道您的参数实际上是第三个命名参数(subject)。在第二个示例中,您正在使用ANY占位符,但到目前为止我还没有使用过它。 - Simeon Visser
这并没有回答问题。在许多情况下(例如,检查记录器是否被调用,但不使测试验证特定的日志消息字符串),您需要检查模拟对象是否已被调用,但您并不关心(也可能不知道)它使用了什么参数。 - ely
问题说明测试中只有一个参数是相关的,这不仅仅是关于断言任何日志调用。 - Simeon Visser

24

使用unittest.mock中的ANY,可以在assert_called_with中使用通配符:

from unittest.mock import ANY

self.myclass.sendmail.assert_called_with(
    subject="Hello World",
    mail_from=ANY,
    mail_to=ANY,
    body=ANY,
    format=ANY,
)

https://docs.python.org/3/library/unittest.mock.html#any


这个答案是我问题的更干净的解决方案。有趣的是,没有一个库实现它... - aydow
创意且符合Python风格 - Freek Wiekmeijer
1
太棒了!在对JSON /字典进行断言时,当某些值难以确定(例如一些生成的项目ID)时,这个建议完美地发挥作用。我使用这种模式来实现AnyInt()AnyISODate(),这使得我的Django / DRF单元测试更加清晰。 - Ihor Pomaranskyy
unittest.mock 自 Python 3.5 起支持 ANY,它的实现与此答案中的 __eq__ 完全相同。请参阅 https://docs.python.org/3/library/unittest.mock.html#any。 - Brendan Batliner
感谢 @BrendanBatliner,我已经更新了它。 - Markis

1
args, _ = your_mock.call_args
assert "must-exist-string" in str(args)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接