Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 457
pymarco Avatar asked Jan 01 '23 00:01

pymarco


2 Answers

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()
like image 148
hoefling Avatar answered Jan 08 '23 18:01

hoefling


I accomplish this doing this(Django 1.11.5)

@patch("django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager")
def test_reverse_mock_count(self, reverse_mock):
    instance = mommy.make(DjangoModel)

    manager_mock = MagicMock
    count_mock = MagicMock()
    manager_mock.count = count_mock()
    reverse_mock.return_value = manager_mock

    instance.related_manager.count()
    self.assertTrue(count_mock.called)

hope this help!

like image 20
Luis Fernando Avatar answered Jan 08 '23 18:01

Luis Fernando