Search code examples
djangodjango-rest-frameworkpytestpython-mockpytest-django

Mocking a RelatedManager in Django 2


This question is directly related to this question, but that one is now outdated it seems.

I am trying to test a view without having to access the database. To do that I need to Mock a RelatedManager on the user.

I am using pytest and pytest-mock.

models.py

# truncated for brevity, taken from django-rest-knox
class AuthToken(models.Model):
    user = models.ForeignKey(
        User, 
        null=False, 
        blank=False,
        related_name='auth_token_set', 
        on_delete=models.CASCADE
    )

views.py

class ChangeEmail(APIView):
    permission_classes = [permissions.IsAdmin]
    serializer_class = serializers.ChangeEmail

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        user = request.user
        user.email = request.validated_data['email']
        user.save()

        # Logout user from all devices
        user.auth_token_set.all().delete() # <--- How do I mock this?

        return Response(status=status.HTTP_200_OK)

test_views.py

def test_valid(mocker, user_factory):
    user = user_factory.build()
    user.id = 1

    data = {
        'email': '[email protected]'
    }

    factory = APIRequestFactory()
    request = factory.post('/', data=data)
    force_authenticate(request, user)

    mocker.patch.object(user, "save")

    related_manager = mocker.patch(
        'django.db.models.fields.related.ReverseManyToOneDescriptor.__set__',
        return_vaue=mocker.MagicMock()
    )
    related_manager.all = mocker.MagicMock()
    related_manager.all.delete = mocker.MagicMock()

    response = ChangeEmail.as_view()(request)
    assert response.status_code == status.HTTP_200_OK

Drawing from the answer in the linked question I tried to patch the ReverseManyToOneDescriptor. However, it does not appear to actually get mocked because the test is still trying to connect to the database when it tries to delete the user's auth_token_set.


Solution

  • You'll need to mock the return value of the create_reverse_many_to_one_manager factory function. Example:

    def test_valid(mocker):
        mgr = mocker.MagicMock()
        mocker.patch(
            'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager', 
            return_value=mgr
        )
    
        user = user_factory.build()
        user.id = 1
        ...
        mgr.assert_called()
    

    Beware that the above example will mock the rev manager for all models. If you need a more fine-grained approach (e.g. patch User.auth_token's rev manager only, leave the rest unpatched), provide a custom factory impl, e.g.

    def test_valid(mocker):
        mgr = mocker.MagicMock()
        factory_orig = related_descriptors.create_reverse_many_to_one_manager
        def my_factory(superclass, rel):
            if rel.model == User and rel.name == 'auth_token_set':
                return mgr
            else:
                return factory_orig(superclass, rel)
    
        mocker.patch(
            'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
            my_factory
        )
    
        user = user_factory.build()
        user.id = 1
        ...
        mgr.assert_called()