断言在Python单元测试中已调用方法


91

假设我在Python单元测试中具有以下代码:

aw = aps.Request("nv1")
aw2 = aps.Request("nv2", aw)

有没有一种简单的方法可以断言aw.Clear()在测试的第二行期间调用了特定方法(在我的情况下)?例如是否有这样的事情:

#pseudocode:
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw))

Answers:


150

我为此使用Mock(在py3.3 +上现在是unittest.mock):

from mock import patch
from PyQt4 import Qt


@patch.object(Qt.QMessageBox, 'aboutQt')
def testShowAboutQt(self, mock):
    self.win.actionAboutQt.trigger()
    self.assertTrue(mock.called)

对于您的情况,它可能看起来像这样:

import mock
from mock import patch


def testClearWasCalled(self):
   aw = aps.Request("nv1")
   with patch.object(aw, 'Clear') as mock:
       aw2 = aps.Request("nv2", aw)

   mock.assert_called_with(42) # or mock.assert_called_once_with(42)

Mock支持许多有用的功能,包括修补对象或模块的方式以及检查是否调用了正确的东西等。

买者自负!(请当心!)

如果您输入错误的assert_called_with(到assert_called_onceassert_called_wiht)您的测试可能仍在运行,因为Mock会认为这是一个模拟的函数并且很乐意进行,除非使用autospec=true。有关更多信息,请阅读assert_call_once:Threat或Menace


5
+1通过精美的Mock模块离散地启发我的世界。
罗恩·科恩

@RonCohen:是的,这非常了不起,而且一直都在进步。:)
Macke 2012年

1
虽然使用模拟绝对是必经之路,但我建议您不要使用assert_call_once,因为它根本不存在:)
FelixCQ 2013年

它已在更高版本中删除。我的测试仍在使用它。:)
Macke

1
值得重复一遍,对任何模拟对象使用autospec = True会很有帮助,因为如果您拼写assert方法,它确实会咬你。
rgilligan '17

30

是的,如果您使用的是Python 3.3+。您可以使用内置的unittest.mock断言方法调用。对于Python 2.6+,请使用rolling backport Mock,这是相同的。

这是您情况下的简单示例:

from unittest.mock import MagicMock
aw = aps.Request("nv1")
aw.Clear = MagicMock()
aw2 = aps.Request("nv2", aw)
assert aw.Clear.called

14

我不知道任何内置的东西。实现起来非常简单:

class assertMethodIsCalled(object):
    def __init__(self, obj, method):
        self.obj = obj
        self.method = method

    def called(self, *args, **kwargs):
        self.method_called = True
        self.orig_method(*args, **kwargs)

    def __enter__(self):
        self.orig_method = getattr(self.obj, self.method)
        setattr(self.obj, self.method, self.called)
        self.method_called = False

    def __exit__(self, exc_type, exc_value, traceback):
        assert getattr(self.obj, self.method) == self.called,
            "method %s was modified during assertMethodIsCalled" % self.method

        setattr(self.obj, self.method, self.orig_method)

        # If an exception was thrown within the block, we've already failed.
        if traceback is None:
            assert self.method_called,
                "method %s of %s was not called" % (self.method, self.obj)

class test(object):
    def a(self):
        print "test"
    def b(self):
        self.a()

obj = test()
with assertMethodIsCalled(obj, "a"):
    obj.b()

这就要求对象本身不会修改self.b,这几乎总是正确的。


我说我的Python生锈了,尽管我确实测试了我的解决方案以确保它能正常工作:-)我在2.5版之前对Python进行了内部化,实际上我从未将2.5用于任何重要的Python,因为为了保持lib的兼容性我们不得不冻结在2.3上。在查看您的解决方案时,我发现effbot.org/zone/python-with-statement.htm是一个很好的清晰描述。我要谦虚地建议,如果您想要多个记录点,而不是嵌套的“ with”,我的方法看起来会更小,并且可能更容易应用。我真的很想让您解释一下您是否有任何特别的好处。
安迪·邓特

@Andy:您的答案较小,因为它是局部的:它实际上没有测试结果,测试后也没有恢复原始功能,因此您可以继续使用该对象,并且必须重复编写代码以完成所有操作每次您编写测试时,都会再次执行该操作。支持代码的行数并不重要;此类进入其自己的测试模块,而不是内嵌在文档字符串中-实际测试中需要一两行代码。
Glenn Maynard

6

是的,我可以给您提纲,但是我的Python有点生疏,我太忙了无法详细解释。

基本上,您需要在将调用原始方法的方法中放置一个代理,例如:

 class fred(object):
   def blog(self):
     print "We Blog"


 class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth

   def __call__(self, code=None):
     self.meth()
     # would also log the fact that it invoked the method

 #example
 f = fred()
 f.blog = methCallLogger(f.blog)

这个关于callable的StackOverflow答案可以帮助您理解以上内容。

更详细地:

尽管答案已被接受,但由于与Glenn进行了有趣的讨论并有几分钟的空闲时间,我希望扩大答案:

# helper class defined elsewhere
class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth
     self.was_called = False

   def __call__(self, code=None):
     self.meth()
     self.was_called = True

#example
class fred(object):
   def blog(self):
     print "We Blog"

f = fred()
g = fred()
f.blog = methCallLogger(f.blog)
g.blog = methCallLogger(g.blog)
f.blog()
assert(f.blog.was_called)
assert(not g.blog.was_called)

很好 我在methCallLogger中添加了呼叫计数,因此可以对其进行断言。
Mark Heath 2010年

这是我提供的全面,独立的解决方案吗?认真吗
格伦·梅纳德

@Glenn我对Python非常陌生-也许您的Python更好-我只是还不了解所有这些。稍后我会花一些时间尝试一下。
Mark Heath 2010年

到目前为止,这是最简单,最容易理解的答案。真的很好!
Matt Messersmith

4

您可以aw.Clear手动或使用像pymox这样的测试框架进行模拟。手动地,您可以使用以下方法:

class MyTest(TestCase):
  def testClear():
    old_clear = aw.Clear
    clear_calls = 0
    aw.Clear = lambda: clear_calls += 1
    aps.Request('nv2', aw)
    assert clear_calls == 1
    aw.Clear = old_clear

使用pymox,您可以这样做:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)

我也喜欢这种方法,尽管我仍然希望调用old_clear。这使正在发生的事情变得显而易见。
Mark Heath 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.