141

Suppose I have the following code in a Python unit test:

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

Is there an easy way to assert that a particular method (in my case aw.Clear()) was called during the second line of the test? e.g. is there something like this:

#pseudocode:
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw))
Mark Heath
  • 48,273
  • 29
  • 137
  • 194

5 Answers5

208

I use Mock (which is now unittest.mock on py3.3+) for this:

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)

For your case, it could look like this:

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 supports quite a few useful features, including ways to patch an object or module, as well as checking that the right thing was called, etc etc.

Caveat emptor! (Buyer beware!)

If you mistype assert_called_with (to assert_called_once or just swap two letters assert_called_wiht) your test may still run, as Mock will think this is a mocked function and happily go along, unless you use autospec=true. For more info read assert_called_once: Threat or Menace.

Macke
  • 24,812
  • 7
  • 82
  • 118
  • 8
    +1 for discretely enlightening my world with the wonderful Mock module. – Ron Cohen Apr 26 '12 at 15:09
  • @RonCohen: Yeah, it's pretty amazing, and getting better all the time too. :) – Macke Apr 26 '12 at 18:52
  • 1
    While using mock is definitely the way to go, I'd advise against using assert_called_once, with simply doesn't exist :) – FelixCQ Jun 19 '13 at 18:09
  • it's been removed in later versions. My tests are still using it. :) – Macke Jun 20 '13 at 05:52
  • Wrt last update and the blog I linked. Now I wonder if my tests did work properly or not. Oh well. That was 4 years ago, at another company. :-| – Macke Oct 09 '15 at 15:16
  • 1
    It's worth repeating how helpful it is to use autospec=True for any mocked object because it can really bite you if you misspell the assert method. – rgilligan Jul 13 '17 at 16:18
  • If you find that this isn't correctly patching it, make sure you are patching in the right place. This may be helpful: https://docs.python.org/3/library/unittest.mock.html#where-to-patch – Brett Jackson Dec 26 '18 at 21:40
  • One another thing which I noticed is when I add this kind of patch unit test case execution time increase a bit though in seconds. When I have normal patch only tests 8 tests took only 0.009 secs and when I added 2 more test cases which used the above method execution time became 1.580 secs – Gaurav Parek Feb 07 '22 at 14:17
  • @FelixCQ assert_called_once() exists in unittest.mock.Mock since Python 3.6. Ref: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_once – Alan Evangelista Jan 24 '23 at 11:08
  • i would like to edit the little typo of "wiht" to "with" - but the platforms edit stack seems to be full. – Alexander Stohr Jun 22 '23 at 12:10
  • 1
    @AlexanderStohr Intentional typo. – Macke Jun 27 '23 at 06:31
32

Yes if you are using Python 3.3+. You can use the built-in unittest.mock to assert method called. For Python 2.6+ use the rolling backport Mock, which is the same thing.

Here is a quick example in your case:

from unittest.mock import MagicMock
aw = aps.Request("nv1")
aw.Clear = MagicMock()
aw2 = aps.Request("nv2", aw)
assert aw.Clear.called
Stefan Collier
  • 4,314
  • 2
  • 23
  • 33
Devy
  • 9,655
  • 8
  • 61
  • 59
14

I'm not aware of anything built-in. It's pretty simple to implement:

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()

This requires that the object itself won't modify self.b, which is almost always true.

Glenn Maynard
  • 55,829
  • 10
  • 121
  • 131
  • I said my Python was rusty, although I did test my solution to make sure it works :-) I internalized Python before version 2.5, in fact I never used 2.5 for any significant Python as we had to freeze at 2.3 for lib compatibility. In reviewing your solution I found http://effbot.org/zone/python-with-statement.htm as a nice clear description. I would humbly suggest my approach looks smaller and might be easier to apply if you wanted more than one point of logging, rather than nested "with"s. I'd really like you to explain if there are any particular benefits of yours. – Andy Dent Oct 01 '10 at 11:08
  • 1
    @Andy: Your answer is smaller because it's partial: it doesn't actually test the results, it doesn't restore the original function after the test so you can continue using the object, and you have to repeatedly write the code to do all that again each time you write a test. The number of lines of support code isn't important; this class goes in its own testing module, not inline in a docstring--this takes one or two lines of code in the actual test. – Glenn Maynard Oct 01 '10 at 20:26
7

Yes, I can give you the outline but my Python is a bit rusty and I'm too busy to explain in detail.

Basically, you need to put a proxy in the method that will call the original, eg:

 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)

This StackOverflow answer about callable may help you understand the above.

In more detail:

Although the answer was accepted, due to the interesting discussion with Glenn and having a few minutes free, I wanted to enlarge on my answer:

# 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)
Community
  • 1
  • 1
Andy Dent
  • 17,578
  • 6
  • 88
  • 115
5

You can mock out aw.Clear, either manually or using a testing framework like pymox. Manually, you'd do it using something like this:

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

Using pymox, you'd do it like this:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)
Max Shawabkeh
  • 37,799
  • 10
  • 82
  • 91
  • I like this approach too, although I still want old_clear to get called. This makes it obvious what's going on. – Mark Heath Sep 30 '10 at 11:04