views:

69

answers:

3

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))
+2  A: 

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
I like this approach too, although I still want old_clear to get called. This makes it obvious what's going on.
Mark Heath
+2  A: 

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)
Andy Dent
nice. I've added a call count to methCallLogger so I can assert on it.
Mark Heath
This over the thorough, self-contained solution I provided? Seriously?
Glenn Maynard
@Glenn I'm very new to Python - maybe your one is better - I just don't understand all of it yet. I'll spend a bit of time later trying it out.
Mark Heath
+1  A: 

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
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
@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
@Andy: Note, I wasn't criticizing you for your answer; this one is just much more complete, providing a complete solution for asserting that a block of code causes a method to be called, which is what was requested. His accepting the quick, general-direction answer and ignoring the thorough one means I probably won't bother answering his questions in the future.
Glenn Maynard