2
votes

Se moquer d'un RelatedManager dans Django 2

Cette question est directement liée à cette question a >, mais celui-ci est maintenant obsolète il semble.

J'essaye de tester une vue sans avoir à accéder à la base de données. Pour ce faire, je dois moquer un RelatedManager sur l'utilisateur.

J'utilise pytest et pytest-mock .

models.py

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

    data = {
        'email': 'foo@example.com'
    }

    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

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

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

En m'inspirant de la réponse à la question liée, j'ai essayé de patcher le ReverseManyToOneDescriptor . Cependant, il ne semble pas être moqué car le test essaie toujours de se connecter à la base de données lorsqu'il tente de supprimer le auth_token_set de l'utilisateur .


0 commentaires

3 Réponses :


1
votes

Si vous utilisez le APITestCase de django, cela devient relativement 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': 'foo@example.com'}
        )

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

Cela évite de se moquer complètement et donne une représentation plus précise de votre logique.


6 commentaires

Merci d'avoir écrit. Cependant, je souhaite tester sans utiliser la base de données. C'est pourquoi j'utilise mocker pour patcher les appels de base de données. Tel est le but de la question. J'utilise également un RequestFactory au lieu du client pour tester la vue directement. Je peux tester avec succès avec un accès db.


Voici un article de blog simple expliquant pourquoi écrire des tests unitaires sans accès à la base de données. chasse -seibert.github.io/blog/2012/07/27/…


Et voici une explication pour savoir pourquoi ne pas utiliser le client . À votre santé. ianlewis.org/en/testing-django-views- sans-utiliser-test-clie‌ nt


@pymarco citant le premier post que vous avez cité, À mon avis, se moquer de la couche de base de données est idiot; on a déjà une abstraction pour ça, ça s'appelle l'ORM. Au lieu de cela, vous pouvez obtenir toute la vitesse dont vous avez besoin en utilisant une base de données sqlite3 en mémoire pour vos tests unitaires. L'utilisation de RequestFactory sur Client est en effet logique dans certains scénarios (par exemple les tests de middleware), je n'ai jamais vu de cas d'utilisation qui nécessiterait de se moquer de la couche ORM.


@hoefling merci d'avoir signalé cette contradiction. J'allais vite et je l'ai négligé. Ma première pensée est préoccupante car Django propose de nombreuses fonctionnalités spécifiques à Postgres. Je vais maintenant considérer sqlite, mais je dois le faire avec prudence. Il ne semble pas idiot de se moquer de l'ORM, car je sais que l'ORM fonctionne, il est donc logique de contrôler sa sortie pendant un test.


Après avoir réfléchi un peu plus et lu pendant la dernière heure, je me sens plus confiant en testant avec Postgres si je vais utiliser l'accès à la base de données. Si la vitesse devient un problème au fur et à mesure que je me développe, je vais me réadresser. Autrement dit, à moins que quelqu'un ne puisse montrer comment patcher le RelatedManager .



4
votes

Vous devrez vous moquer de la valeur de retour de la fonction d'usine create_reverse_many_to_one_manager . Exemple:

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

Attention, l'exemple ci-dessus se moquera du gestionnaire de rév pour tous les modèles. Si vous avez besoin d'une approche plus fine (par exemple, le gestionnaire de révision du correctif User.auth_token uniquement, laissez le reste non corrigé), fournissez un implément d'usine personnalisé, par exemple

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


1 commentaires

Wow, c'est génial! Merci d'avoir compris cela



0
votes

J'accomplis cela en faisant ceci (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)

espère cette aide!


0 commentaires