Mocking a RelatedManager in Django 2
Asked Answered
P

4

3

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.

Provisional answered 29/4, 2019 at 16:41 Comment(0)
P
6

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()
Penalty answered 29/4, 2019 at 20:56 Comment(1)
Wow, that is awesome! Thank you for figuring that outProvisional
A
2

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!

Alton answered 3/9, 2019 at 15:17 Comment(0)
D
1

If you use django's APITestCase, this becomes relatively simple.

class TestChangeEmail(APITestCase):
    def test_valid(self):
        user = UserFactory()
        auth_token = AuthToken.objects.create(user=user)

        response = self.client.post(
            reverse('your endpoint'), 
            data={'email': '[email protected]'}
        )

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertFalse(AuthToken.objects.filter(user=user).exists())

This avoids mocking altogether and gives a more accurate representation of your logic.

Deglutinate answered 29/4, 2019 at 16:52 Comment(6)
Thank you for writing. However, I want to test without using the database. That is why I am using mocker to patch the database calls. That is the purpose of the question. Also I am using a RequestFactory instead of the client to test the view directly. I can successfully test with db access.Provisional
Here is a simple blog post explaining why to write unit tests w/o db access. chase-seibert.github.io/blog/2012/07/27/…Provisional
And here is an explanation as to why to not use the client. Cheers. ianlewis.org/en/testing-django-views-without-using-test-clientProvisional
@Provisional quoting the first post you cited, In my opinion, mocking out the database layer is silly; we already have an abstraction for that, it's called the ORM. Instead, you can get all the speed you need by using an in memory sqlite3 database for your unit tests. While using RequestFactory over Client indeed makes sense in some scenarios (e.g. middleware testing), I've never seen a use case that would require mocking the ORM layer.Penalty
@Penalty thanks for pointing out that contradiction. I was going fast and overlooked it. My initial thought is one of concern since Django offers a lot of Postgres specific functionality. I will now consider sqlite, but need to do so cautiously. It doesn't seem silly to mock the ORM to me b/c I know the ORM works so it makes sense to control it's output during a test.Provisional
After thinking about it some more, and reading around for the last hour, I feel more confident testing with Postgres if I'm going to use db access. If speed does become an issue as I expand out then I'll readdress. That is, unless someone is able to show how to patch the RelatedManager.Provisional
D
1

unittest.PropertyMock can be used to mock descriptors in a way that doesn't require mocking internal implementation details:

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")

    with mocker.patch('app.views.User.auth_token_set', new_callable=PropertyMock) as mock_auth_token_set:
        mock_delete = mocker.MagicMock()
        mock_auth_token_set.return_value.all.return_value.delete = mock_delete

        response = ChangeEmail.as_view()(request)

    assert response.status_code == status.HTTP_200_OK
    assert mock_delete.call_count == 1
Devotion answered 21/12, 2021 at 14:8 Comment(1)
This seems to be the most "pytestic" way.Mickelson

© 2022 - 2024 — McMap. All rights reserved.