5

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?

2 Answers 2

7

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)]
Sign up to request clarification or add additional context in comments.

5 Comments

Is assertRaises supposed to get a specific exception? In the real code at some point i have a condition after getting the results that checks the result set for a key. if the key is found then raises the exception. In the test code: with patch('yyyyy.Client') as MockClient: context_manager = MockClient.create_from_token.return_value mock_client = context_manager.__enter__.return_value mock_client.get.return_value = response self.assertRaises(PermissionDenied, foo, 1, self.token) AssertRaises doesn't catch the exeption.
I can clearly see the logs being triggered near my raise PermissionDenied
@Susy11: that'd mean that either the foo(1, self.token) call did not raise the exception at that point but before perhaps, or that the PermissionDenied class you have there is not the same PermissionDenied class that was raised. Note that you can use self.assertRaises() as a context manager: with self.assertRaises(PermissionDenied): (next line further indented) foo(1, self.token).
@Susy11: if you have two different class objects that are both named PermissionDenied, you can detect this by using try: foo(1, self.token), except Exception as e: print(id(type(e)), id(PermissionDenied), type(e) is PermissionDenied). If that prints two identical ids and True, you do have the correct exception class.
got it working, by having the except block at the the end of the real code the exception was raised but caught and treated. Thank you for the quick answer on this it helped me a great deal understanding mocking in python
2

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.