简体   繁体   中英

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.

Could you add this to myTest.py and try

from someLib import web_utils
web_utils.SomeHTTPClient = <YOUR MOCK CLASS>

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

I know this isn't exactly what you wanted, but after creating your mock class I would simply do something like this, to make the code more clean (in the bash shell, otherwise if you have 1 file you could just do a find and replace)

find . -name "*.py" -type f -exec sed -i "s/<old_class>/<mock_class>/g" '{}' \;

That should replace all instances of your old class with your new class, and I personally think it would make for much cleaner code. If you want just one file, instead of all .py files, just change -name to "file.py"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM