Search code examples
pythondjangounit-testingmockingpython-mock

How to mock class attribute instantiated in __init__?


I'm trying to mock the "self.api.friends.get" method in VKAuth class:

import vk

class VKAuth(object):
    def __init__(self, access_token, user):
        self.session = vk.Session(access_token = access_token)
        self.api = vk.API(self.session)

    def follow(self):
        vk_friends = self.api.friends.get()

from the test module test_views.py:

from mock import patch
from ..auth_backends.vk_backend import VKAuth

class AddUsersToList(TestCase):
    def test_auth_vk(self, mock_get):
         ... etc ...
        auth_token = 'ceeecdfe0eb4bf68ceeecdfe0eb4bf68ceeecdfe0eb4bf68652530774ced6cbc8cba0'
        token = user.auth_token.key
        self.client.defaults['HTTP_AUTHORIZATION'] = 'Token {}'.format(token)
        with patch.object(accounts.auth_backends.vk_backend.VKAuth, 'api'): #point where we're mocking
            response = self.client.post(reverse('auth-social', kwargs=dict(backend='vk')), dict(access_token=auth_token), follow=True)

having created instance of VKAuth class during the post call to 'auth-social' above in SNView class-based view:

class SNView(generics.GenericAPIView):
    serializer_class = serializers.AuthSocialSerializer
    permission_classes = (rest_permissions.IsAuthenticated)

    def post(self, request, backend, *args, **kwargs):
        s = self.get_serializer(data=request.DATA)

        if s.is_valid():
            auth_backends = {
                'vk': VKAuth,
                'facebook': FBAuth
            }

            if backend in auth_backends:
                auth_backend = auth_backends[backend](access_token=s.data['access_token'], user=self.request.user)

And I get an error:

AttributeError: <class 'accounts.auth_backends.vk_backend.VKAuth' doens't have the attribute 'api'

What should I write istead of the current patch.object to reach api.friends.get and mock it?

UPD:

To be more precise, I want some equivalent of:

    auth_token = 'ceeecdfe0eb4bf68ceeecdfe0eb4bf68ceeecdfe0eb4bf68652530774ced6cbc8cba0'
    user = User.objects.get(id = 2)
    vk_auth = VKAuth(auth_token, user)

    vk_ids=[111111,2222222,3333333,44444444]
    vk_auth.authenticate()
    vk_auth.api.friends = MagicMock(name='get', return_value=None)
    vk_auth.api.friends.get = MagicMock(name='get', return_value=vk_ids)
    data = vk_auth.follow()

but mock it the moment before we make request to the django-rest-framework api via self.client.post().

Thank you!


Solution

  • You are patching the wrong thing. In VKAuth:

    self.api = vk.API(self.session)
    

    add api attribute to VKAuth self object. When you call

    patch.object(accounts.auth_backends.vk_backend.VKAuth, 'api')
    

    you are patching the api static attribute of VKAuth class and not the object attribute.

    You should patch vk.API instead.

    with patch('vk.API', autospec=True) as mock_api:
        response = self.client.post(reverse('auth-social', kwargs=dict(backend='vk')), dict(access_token=auth_token), follow=True)
    

    Notes :

    1. Use patch.object only if you really know why you need it instead simple patch.
    2. autospec=True is not mandatory but I strongly encourage to use it.
    3. In the patch context self.api will be equal to mock_api.return_value because call vk.API(self.session) is like call mock_api(); in other words mock_api is the mock object used to replace the vk.API reference.
    4. Take a look to where to patch, you can find it very useful.

    Now if you want fill your mock_api.return_value by some behavior you can configure it in with context:

    with patch('vk.API', autospec=True) as mock_api:
        m_api = mock_api.return_value
        m_api.friends.return_value = None
        m_api.friends.get.return_value = vk_ids
        .... Your test