Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I patch an object so that all methods are mocked except one?

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!

like image 397
bbengfort Avatar asked Mar 03 '16 18:03

bbengfort


People also ask

What is mock patch?

mock provides a powerful mechanism for mocking objects, called patch() , which looks up an object in a given module and replaces that object with a Mock . Usually, you use patch() as a decorator or a context manager to provide a scope in which you will mock the target object.

What is Side_effect in mock python?

side_effect: A function to be called whenever the Mock is called. See the side_effect attribute. Useful for raising exceptions or dynamically changing return values. The function is called with the same arguments as the mock, and unless it returns DEFAULT , the return value of this function is used as the return value.


2 Answers

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.

like image 99
Michele d'Amico Avatar answered Oct 21 '22 11:10

Michele d'Amico


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!

like image 33
Ezra Avatar answered Oct 21 '22 11:10

Ezra