Susy11
Susy11

Reputation: 260

Python function not using the mocked object

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

Answers (2)

hspandher
hspandher

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

Martijn Pieters
Martijn Pieters

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

Related Questions