Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock a Python class that is two imports deep?

How can I mock a Python class that's two imports deep, without changing code in either of the imported modules? Say I'm importing a library of web utilities, which imports an HTTPClient() - how can I write a unit test, that mocks the HTTPClient to return a value, without changing the file web_utils.py? I want to use the data manipulation in DataHandler (rather than mock it out), but I don't want the HTTPClient to actually connect to the web.

Is this even possible? Given that Python has monkey-patching, it certainly seems like it should be. Or is there an alternative/better way? I'm still figuring out the mocking process, much less changing imports.

# someLib/web_utils.py
from abc.client import SomeHTTPClient # the class to replace

def get_client():
    tc = SomeHTTPClient(endpoint='url') # fails when I replace the class
    return tc

class DataHandler(object):
    def post_data(someURL, someData):
        newData = massage(someData)
        client = get_client()
        some_response = client.request(someURL, 'POST', newData)
        return some_response

# code/myCode.py
from someLib.web_utils import DataHandler

dh = DataHandler()
reply = dh.post_data(url, data)

# tests/myTests.py
from django.test.testcases import TestCase
from mock import Mock

class Mocking_Test(TestCase):
    def test_mock(self):
        from someLib import web_utils
        fakeClient = Mock()
        fakeClient.request = web_utils.SomeHTTPClient.request # just to see if it works
        web_utils.SomeHTTPClient = fakeClient
        dh = DataHandler()
        reply = dh.post_data(url='somewhere', data='stuff')

Update - added the get_client() function. I think @spicavigo's answer is on the right track - it does appear to be replacing the SomeHTTPClient class. But for some reason the class doesn't instantiate an object (the error is, "must be type, not Mock"). I don't see how it could, either, being a Mock() object that has been created, rather than a class. So I'm not sure how to make that part work.

like image 260
John C Avatar asked Sep 06 '12 13:09

John C


People also ask

How do you mock a class method in Unittest Python?

Since you want to mock out the method with a custom implementation, you could just create a custom mock method object and swap out the original method at testing runtime. I want to do a custom implementation of loaddata method , rather than just do the return value. You can assign loaddata to the custom implementation.

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.

What is the difference between mock and MagicMock?

With Mock you can mock magic methods but you have to define them. MagicMock has "default implementations of most of the magic methods.". If you don't need to test any magic methods, Mock is adequate and doesn't bring a lot of extraneous things into your tests.

When should you not use a mock?

Only use a mock (or test double) “when testing things that cross the dependency inversion boundaries of the system” (per Bob Martin). If I truly need a test double, I go to the highest level in the class hierarchy diagram above that will get the job done. In other words, don't use a mock if a spy will do.


2 Answers

Could you add this to myTest.py and try

from someLib import web_utils
web_utils.SomeHTTPClient = <YOUR MOCK CLASS>
like image 122
spicavigo Avatar answered Sep 21 '22 18:09

spicavigo


Here's what finally worked:

class Mocking_Test(TestCase):
    def test_mock(self):
        def return_response(a, b, c, *parms, **args):
            print "in request().return_response"
            class makeResponse(object):
                status = 200
                reason = "making stuff up"
            return makeResponse()

        fakeClient = Mock()
        fakeClient.return_value = return_response

        #with patch('abc.client.SomeHTTPClient') as MockClient: # not working.
        with patch('abc.client.SomeHTTPClient.request', new_callable=fakeClient):
            dh = DataHandler()
            reply = dh.post_data(url='somewhere', data='stuff')

I was on the right track in my original post, with the patch, I simply wasn't doing the patching correctly. Also, rather than replacing the entire class, I'm replacing just the request function on the class (which is all that needs to be mocked out, to avoid actually making an HTTP request).

like image 34
John C Avatar answered Sep 23 '22 18:09

John C