在 Django 2 中模拟 RelatedManager
Mocking a RelatedManager in Django 2
这个问题与 this question 直接相关,但那个问题现在看来已经过时了。
我正在尝试在不访问数据库的情况下测试视图。为此,我需要在用户身上模拟 RelatedManager
。
我正在使用 pytest
和 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': '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
.
时仍在尝试连接到数据库
如果使用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())
这完全避免了模拟,并更准确地表示了您的逻辑。
您需要模拟 create_reverse_many_to_one_manager
工厂函数的 return 值。示例:
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 管理器,其余部分不打补丁),请提供自定义工厂实现,例如
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()
我这样做了(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)
希望对您有所帮助!
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
这个问题与 this question 直接相关,但那个问题现在看来已经过时了。
我正在尝试在不访问数据库的情况下测试视图。为此,我需要在用户身上模拟 RelatedManager
。
我正在使用 pytest
和 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': '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
.
如果使用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())
这完全避免了模拟,并更准确地表示了您的逻辑。
您需要模拟 create_reverse_many_to_one_manager
工厂函数的 return 值。示例:
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 管理器,其余部分不打补丁),请提供自定义工厂实现,例如
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()
我这样做了(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)
希望对您有所帮助!
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