在 Django 2 中模拟 RelatedManager

Posted

技术标签:

【中文标题】在 Django 2 中模拟 RelatedManager【英文标题】:Mocking a RelatedManager in Django 2 【发布时间】:2019-09-18 07:52:17 【问题描述】:

这个问题与this question直接相关,但现在看来这个问题已经过时了。

我试图在不访问数据库的情况下测试视图。为此,我需要在用户身上模拟一个 RelatedManager

我正在使用pytestpytest-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': '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

根据链接问题中的答案,我尝试修补 ReverseManyToOneDescriptor。但是,它似乎并没有真正被嘲笑,因为当它试图删除用户的auth_token_set 时,测试仍在尝试连接到数据库。

【问题讨论】:

【参考方案1】:

您需要模拟create_reverse_many_to_one_manager 工厂函数的返回值。示例:

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

请注意,上面的示例将模拟所有模型的转速管理器。如果您需要更细粒度的方法(例如,仅修补 User.auth_token 的 rev 管理器,其余部分不打补丁),请提供自定义工厂 impl,例如

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

【讨论】:

哇,太棒了!感谢您解决这个问题【参考方案2】:

我这样做(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)

希望对您有所帮助!

【讨论】:

【参考方案3】:

如果使用django的APITestCase,这就变得比较简单了。

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

这完全避免了模拟,并更准确地表示您的逻辑。

【讨论】:

感谢您的来信。但是,我想在不使用数据库的情况下进行测试。这就是为什么我使用 mocker 来修补数据库调用。这就是问题的目的。我也使用RequestFactory 而不是client 直接测试视图。我可以通过数据库访问成功测试。 这是一篇简单的博客文章,解释了为什么要编写没有数据库访问权限的单元测试。 chase-seibert.github.io/blog/2012/07/27/… 这里解释了为什么不使用client。干杯。 ianlewis.org/en/testing-django-views-without-using-test-client @pymarco 引用您引用的第一篇文章,在我看来,模拟数据库层是愚蠢的;我们已经有了一个抽象,它被称为 ORM。相反,您可以通过使用内存中的 sqlite3 数据库进行单元测试来获得所需的所有速度。 虽然在某些情况下(例如中间件测试)使用 RequestFactory 而不是 Client 确实有意义,但我已经从未见过需要模拟 ORM 层的用例。 在考虑了更多,并阅读了最后一个小时后,如果我要使用数据库访问,我对使用 Postgres 进行测试更有信心。如果随着我的扩展速度确实成为问题,那么我将重新解决。也就是说,除非有人能够展示如何修补 RelatedManager【参考方案4】:

unittest.PropertyMock 可用于以不需要模拟内部实现细节的方式模拟描述符:

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

    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

【讨论】:

以上是关于在 Django 2 中模拟 RelatedManager的主要内容,如果未能解决你的问题,请参考以下文章

在 Django 中模拟模型字段验证器

python 模拟Django在单元测试中的缓存

在 Django 的基于类的视图中模拟函数

django:使用 BInaryField 模拟标志字段

django 测试中的模拟时间问题:使用 freezegun 似乎没有冻结时间

Django在批量插入/更新/删除时“模拟”数据库触发行为