断言在Python单元测试中调用了一个方法

Assert that a method was called in a Python unit test

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

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

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

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

我使用mock(现在是py3.3+上的unittest.mock)来完成以下操作:

1
2
3
4
5
6
7
8
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)

对于您的案例,可能如下所示:

1
2
3
4
5
6
7
8
9
10
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_called ou once:threat or menace。


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

下面是一个简单的例子:

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


我不知道有什么内置的。实现起来非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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有点生锈,我太忙了,无法详细解释。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 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答案可以帮助您理解上面的内容。

更详细地说:

尽管答案被接受了,但由于与格伦的讨论很有趣,并且有几分钟的空闲时间,我想进一步讨论我的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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)


您可以手动或使用Pymox之类的测试框架模拟aw.Clear。手动操作时,您可以使用如下方法:

1
2
3
4
5
6
7
8
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,你可以这样做:

1
2
3
4
5
6
class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)