18

I have an entry point function call it main on an object that I would like to remain unmocked, since it calls several other methods on the object:

class Thing(object):

    def main(self):
        self.alpha()
        self.bravo()

    def alpha(self):
        self.charlie()

    def bravo(self):
        raise TypeError("Requires Internet connection!")

    def charlie(self):
        raise Exception("Bad stuff happens here!")

This is pretty straight forward to mock manually:

thing = Thing() 
thing.alpha = MagicMock()
thing.bravo = MagicMock()

And I can test to make sure that alpha and bravo are both called once, I can set side effects in alpha and bravo to make sure they're handled, etc. etc.

What I'm worried about is if the code definition changes and someone adds a charlie call to main. It's not mocked, so now the side effects will be felt (and they're things like writing to a file, connecting to a database, fetching stuff from the Internet, so that simple exception is not going to alert me that the tests are now bad).

My plan was to verify that my mock object calls no other methods than the ones I say it should (or raise a test exception). However if I do something like this:

MockThing = create_autospec(Thing)
thing = Thing()
thing.main() 

print thing.method_calls
# [calls.main()] 

Then main is also mocked so it calls no other methods. How can I mock every method but the main method? (I'd like method_calls to be [calls.alpha(), calls.bravo()]).

Edit: For Hacking Answers

Well, I have a really hacky solution, but I hope there is a better answer than this. Basically I rebind the method from the original class (Python bind an unbound method)

MockThing = create_autospec(Thing)
thing = MockThing()
thing.main = Thing.ingest.__get__(thing, Thing)
thing.main()

print thing.method_calls
# [calls.alpha(), calls.bravo()]

But there has to be a simpler solution than using function descriptors!

Community
  • 1
  • 1
bbengfort
  • 4,572
  • 3
  • 38
  • 54
  • 1
    I get error saying "object has no attribute 'ingest", any idea what caused that? I'm using Python 2.7 – Nam G VU Mar 23 '17 at 05:02

3 Answers3

8

When I did these strange kind of stuff like call a real method of a class that I would mock I used to call the method static reference:

mt = Mock(Thing)
Thing.main(mt)
print(mt.mock_calls)

[call.alpha(), call.bravo()]

Anyway after write your test it is better to separate by use some collaborators to separate what you should mock from what you want to test: use these tests to lead the production code refactor and finally refactor your test to remove these kind of dirty tests.

Michele d'Amico
  • 17,181
  • 7
  • 56
  • 70
3

I had the same issue, but I figured out a way to do it that I'm happy with. The example below uses your Thing class as listed above:

import mock

mock_thing = mock.create_autospec(Thing)
mock_thing.main = lambda x: Thing.main(mock_thing, x)

This will result in mock_thing calling an actual 'main' function belonging to mock_thing, but mock_thing.alpha() and mock_thing.beta() will both be called as mocks! (replace x with any parameters you're passing in to the function).

Hopefully that works for you!

Ezra
  • 33
  • 3
-1

It seems like you're either letting unit testing into your integration/functional tests, or you're worried about testing other things besides a certain unit.

If you're unit testing Thing then you should be mocking the parts that you're not testing. But if you're testing how Thing integrates with other things (such as the DB) then you should be testing your actual Thing, not a mocked up one.

Also, this is where dependency injection makes a lot of sense, because you would do something like this:

class Thing:
    def __init__(self, db, dangerous_thing):
        self.db = db
        self.dangerous_thing = dangerous_thing

    #....

    def charlie(self):
        foxtrot = self.dangerous_thing.do_it()

Now you can pass in a mock for the dangerous_thing while you're testing Thing out so you don't have to worry about really doing the dangerous_thing.

Wayne Werner
  • 41,650
  • 21
  • 173
  • 260