I am trying to patch/mock a method (AsyncHTTPClient.fetch
) that is being called in two different places in my program: Firstly in tornado_openapi3.testing
and secondly in my_file
. The problem is that the method is being patched in the first location, which breaks the functionality of my tests.
my_file.py:
import tornado
class Handler(tornado.web.RequestHandler, ABC):
def initialize(self):
<some_code>
async def get(self, id):
<some_code>
client = AsyncHTTPClient()
response = await client.fetch(<some_path>)
<some_code>
test_handler.py:
from tornado_openapi3.testing import AsyncOpenAPITestCase
class HandlerTestCase(AsyncOpenAPITestCase):
def get_app(self) -> Application:
return <some_app>
def test_my_handler(self):
with patch.object(my_file.AsyncHTTPClient, 'fetch') as mock_fetch:
f = asyncio.Future()
f.set_result('some_result_for_testing')
mock_fetch.return_value = f
self.fetch(<some_path>)
From what I understood from various mocking tutorials (e.g. https://docs.python.org/3/library/unittest.mock.html), fetch
should only be patched/mocked in my_file
. How can I make sure that is the case?
CodePudding user response:
Cause of the issue
The imported class AsyncHTTPClient
in my_file.py
is actually just a reference to tornado's original AsyncHTTPClient
class.
Basically, from x import y
statements are variable assignments in that a new variable called y
is created in the current file referencing the original object x.y
.
And, since, classes are mutable objects, when you patch the fetch
method in the imported class, you are actually patching the fetch
method on the original class.
Here's an example using variable assignment to illustrate this issue:
class A:
x = 1
b = A # create a variable 'b' referencing the class 'A'
b.x = 2 # change the value of 'x' attribute' of 'b'
print(A.x)
# Outputs -> 2 (not 1 because classes are mutable)
Like I said earlier, from ... import ...
statements are basically variable assignments. So the above illustration is what's really happening when you patch the fetch
method.
Solution
Instead of patching a single method, patch the whole class:
with patch.object(my_file, 'AsyncHTTPClient') as mock_client:
f = asyncio.Future()
f.set_result('some_result_for_testing')
mock_client.fetch.return_value = f
self.fetch(<some_path>)
What's happening this time is Python is reassigning the value of the local variable AsyncHTTPClient
to a mock object. There's no mutation going on this time and, so, the original class is not affected.