【问题标题】:Assert that a method was called in a Python unit test断言在 Python 单元测试中调用了一个方法
【发布时间】:2011-04-19 06:54:52
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: python unit-testing


    【解决方案1】:

    我为此使用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 支持很多有用的功能,包括修补对象或模块的方法,以及检查是否调用了正确的东西等等。

    Caveat emptor!(买家小心!)

    如果您输入错误assert_called_with(到assert_called_onceassert_called_wiht),您的测试可能仍会运行,因为Mock 会认为这是一个模拟函数并愉快地继续,除非您使用autospec=true。更多信息请阅读assert_called_once: Threat or Menace

    【讨论】:

    • +1 用精彩的 Mock 模块离散地启发我的世界。
    • @RonCohen:是的,这非常了不起,而且一直在变得更好。 :)
    • 虽然使用 mock 肯定是要走的路,但我建议不要使用 assert_call_once,因为根本不存在 :)
    • 它在以后的版本中被移除了。我的测试仍在使用它。 :)
    • 值得重申的是,对任何模拟对象使用 autospec=True 是多么有帮助,因为如果你拼错了 assert 方法,它真的会咬你。
    【解决方案2】:

    是的,如果您使用的是 Python 3.3+。您可以使用内置的unittest.mock 来断言调用的方法。对于 Python 2.6+,使用回滚端口 Mock,这也是一样的。

    下面是你的例子:

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

    【讨论】:

      【解决方案3】:

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

      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冻结在 2.3 以实现 lib 兼容性。在查看您的解决方案时,我发现 effbot.org/zone/python-with-statement.htm 是一个很好的清晰描述。我会谦虚地建议我的方法看起来更小,如果您想要多个日志记录点,而不是嵌套的“with”,可能更容易应用。如果您有什么特别的好处,我真的希望您解释一下。
      • @Andy:您的答案较小,因为它是部分的:它实际上并没有测试结果,它不会在测试后恢复原始功能,因此您可以继续使用该对象,并且您有每次编写测试时重复编写代码以再次执行所有操作。支持代码的行数并不重要;这个类放在它自己的测试模块中,而不是内联在文档字符串中——这在实际测试中需要一两行代码。
      【解决方案4】:

      是的,我可以给你大纲,但是我的 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)
      

      这个StackOverflow answer关于callable可以帮助你理解以上内容。

      更详细:

      虽然答案被接受了,但由于与格伦的有趣讨论并有几分钟的空闲时间,我想扩大我的答案:

      # 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 添加了调用计数,因此我可以对其进行断言。
      • 这是我提供的彻底、独立的解决方案吗?认真的吗?
      • @Glenn 我对 Python 很陌生——也许你的更好——我只是还没有完全理解它。稍后我会花点时间尝试一下。
      • 这是迄今为止最简单、最容易理解的答案。真的很棒!
      【解决方案5】:

      您可以手动模拟 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 被调用。这让事情变得一目了然。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-08-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-18
      • 2016-03-25
      • 1970-01-01
      相关资源
      最近更新 更多