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.
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.
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.
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.
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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With