Reputation: 260
Coming from a PHP background I have encountered the following issue in writing Python unit tests:
I have function foo that uses a Client object in order to get a response from some other API:
from xxxx import Client
def foo (some_id, token):
path = 'some/api/path'
with Client.get_client_auth(token) as client:
response = client.get(path,params).json()
results = list(response.keys())
.............
For this I have created the following unit test in another python file.
from yyyy import foo
class SomethingTestCase(param1, param2):
def test_foo(self):
response = [1,2,3]
with patch('xxxx.Client') as MockClient:
instance = MockClient.return_value
instance.get.return_value = response
result = foo(1,self.token)
self.assertEqual(response,result)
I don't understand why foo isn't using the mocked [1,2,3] list and instead tries to connect to the actual API path in order to pull the real data.
What am I missing?
Upvotes: 5
Views: 4786
Reputation: 16733
You need to patch the Client
object in the file where it is going to be used, and not in its source file. By the time, your test code is ran, the Client object would already have been imported into the file where you are hitting the API.
# views.py
from xxxx import Client
# test_file.py
...
with patch('views.Client') as MockClient: # and not 'xxxx.Client'
...
Moreover, since you're patching a context manager you need to provide a stub.
Upvotes: 2
Reputation: 1121386
You are doing 3 things wrong:
You are patching the wrong location. You need to patch the yyyy.Client
global, because that's how you imported that name.
The code-under-test is not calling Client()
, it uses a different method, so the call path is different.
You are calling the code-under-test outside the patch lifetime. Call your code in the with
block.
Let's cover this in detail:
When you use from xxxx import Client
, you bind a new reference Client
in the yyyy
module globals to that object. You want to replace that reference, not xxxx.Client
. After all, the code-under-test accesses Client
as a global in it's own module. See the Where to patch section of the unittest.mock
documentation.
You are not calling Client
in the code. You are using a class method (.get_client_auth()
) on it. You also then use the return value as a context manager, so what is assigned to client
is the return value of the __enter__
method on the context manager:
with Client.get_client_auth(token) as client:
You need to mock that chain of methods:
with patch('yyyy.Client') as MockClient:
context_manager = MockClient.get_client_auth.return_value
mock_client = context_manager.__enter__.return_value
mock_client.get.return_value = response
result = foo(1,self.token)
You need to call the code under test within the with
block, because only during that block will the code be patched. The with
statement uses the patch(...)
result as a context manager. When the block is entered, the patch is actually applied to the module, and when the block exits, the patch is removed again.
Last, but not least, when trying to debug such situations, you can print out the Mock.mock_calls
attribute; this should tell you what was actually called on the object. No calls made? Then you didn't patch the right location yet, forgot to start the patch, or called the code-under-test when the patch was no longer in place.
However, if your patch did correctly apply, then MockClient.mock_calls
will look something like:
[call.get_client_auth('token'),
call.get_client_auth().__enter__(),
call.get_client_auth().__enter__().get('some/api/path', {'param': 'value'}),
call.get_client_auth().__exit__(None, None, None)]
Upvotes: 7