如何在 Django rest 框架中为 'delete' 操作编写测试
How to write a test for 'delete' operation in Django rest framework
我正在为我的 Django Rest Framework 编写测试 API。
我一直在测试 'delete'。
我对 'create' 的测试工作正常。
这是我的测试代码:
import json
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from users.models import CustomUser
from lists.models import List, Item
class ListAPITest(APITestCase):
@classmethod
def setUp(self):
self.data = {'name': 'Test list', 'description':'A description', 'item': [
{'name': 'Item 1 Name', 'description': 'Item 1 description', 'order': 1},
{'name': 'Item 2 Name', 'description': 'Item 2 description', 'order': 2},
{'name': 'Item 3 Name', 'description': 'Item 3 description', 'order': 3},
{'name': 'Item 4 Name', 'description': 'Item 4 description', 'order': 4},
{'name': 'Item 5 Name', 'description': 'Item 5 description', 'order': 5},
{'name': 'Item 6 Name', 'description': 'Item 6 description', 'order': 6},
{'name': 'Item 7 Name', 'description': 'Item 7 description', 'order': 7},
{'name': 'Item 8 Name', 'description': 'Item 8 description', 'order': 8},
{'name': 'Item 9 Name', 'description': 'Item 9 description', 'order': 9},
{'name': 'Item 10 Name', 'description': 'Item 10 description', 'order': 10}
]}
# 'lists' is the app_name set in endpoints.py
# 'Lists' is the base_name set for the list route in endpoints.py
# '-list' seems to be something baked into the api
self.url = reverse('lists:Lists-list')
def test_create_list_authenticated(self):
"""
Ensure we can create a new list object.
"""
user = CustomUser.objects.create(email='person@example.com', username='Test user', email_verified=True)
self.client.force_authenticate(user=user)
response = self.client.post(self.url, self.data, format='json')
list_id = json.loads(response.content)['id']
# the request should succeed
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# there should now be 1 List in the database
self.assertEqual(List.objects.count(), 1)
def test_delete_list_by_owner(self):
"""
delete list should succeed if user created list
"""
user = CustomUser.objects.create(email='person@example.com', username='Test user', email_verified=True)
new_list = List.objects.create(name='Test list', description='A description', created_by=user, created_by_username=user.username)
self.client.force_authenticate(user=user)
response = self.client.delete(self.url + '/' + str(new_list.id))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
我看到的不是预期的状态 204:
AssertionError: 405 != 204
405 是不允许的方法。
这是我的模型定义:
class List(models.Model):
"""Models for lists
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_by = models.ForeignKey(USER, on_delete=models.CASCADE, related_name='list_created_by_id')
created_by_username = models.CharField(max_length=255) # this shold be OK given that the list will be deleted if the created_by_id user is deleted
created_at = models.DateTimeField(auto_now_add=True)
parent_item = models.ForeignKey('Item', on_delete=models.SET_NULL, null=True, related_name='parent_item')
modified_by = models.ForeignKey(USER, on_delete=models.SET_NULL, null=True,
related_name='list_modified_by')
modified_at = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=255)
description = models.CharField(max_length=5000, blank=True, default='')
is_public = models.BooleanField(default=False)
def __str__(self):
return self.name
这是我的观点:
class ListViewSet(FlexFieldsModelViewSet):
"""
ViewSet for lists.
"""
permission_classes = [IsOwnerOrReadOnly, HasVerifiedEmail]
model = List
serializer_class = ListSerializer
permit_list_expands = ['item']
pagination_class = LimitOffsetPagination
def get_queryset(self):
# unauthenticated user can only view public lists
queryset = List.objects.filter(is_public=True)
# authenticated user can view public lists and lists the user created
# listset in query parameters can be additional filter
if self.request.user.is_authenticated:
listset = self.request.query_params.get('listset', None)
if listset == 'my-lists':
queryset = List.objects.filter(created_by=self.request.user)
elif listset == 'public-lists':
queryset = List.objects.filter(is_public=True)
else:
queryset = List.objects.filter(
Q(created_by=self.request.user) |
Q(is_public=True)
)
# allow filter by URL parameter created_by
created_by = self.request.query_params.get('created_by', None)
if created_by is not None:
queryset = queryset.filter(created_by=created_by)
# return only lists that have no parent item
toplevel = self.request.query_params.get('toplevel')
if toplevel is not None:
queryset = queryset.filter(parent_item=None)
return queryset.order_by('name')
我已阅读 docs,但我无法找到如何设置删除请求。
我也试过这个:
kwargs = {'pk': new_list.id}
response = self.client.delete(self.url, **kwargs)
这给了我一个错误:
AssertionError: Expected view ListViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
通过我的 React 前端中的 API 在我的应用程序中删除工作正常。
我知道我的对象名为 List 令人困惑...但很难想到另一个名称,因为它就是这样!
感谢您提供我在这里遗漏的任何想法!
我建议你看看 Django-restframework 测试文档。
https://www.django-rest-framework.org/api-guide/testing/
这是我将如何针对您的当前情况编写测试的示例。
from rest_framework.test import APIRequestFactory, force_authenticate
from django.test import TestCase
class TestsAPIListDetailView(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
# This only matters if you are passing url query params e.g. ?foo=bar
self.baseUrl = "/list/"
def test_delete_with_standard_permission(self):
# Creates mock objects
user = CustomUser.objects.create(email='person@example.com', username='Test user', email_verified=True)
new_list = List.objects.create(name='Test list', description='A description', created_by=user,
created_by_username=user.username)
# Creates a mock delete request.
# The url isn't strictly needed here. Unless you are using query params e.g. ?q=bar
req = self.factory.delete("{}{}/?q=bar".format(self.baseUrl, new_list.pk))
current_list_amount = List.object.count()
# Authenticates the user with the request object.
force_authenticate(req, user=user)
# Returns the response data if you ran the view with request(e.g if you called a delete request).
# Also you can put your url kwargs(For example for /lists/<pk>/) like pk or slug in here. Theses kwargs will be automatically passed to view.
resp = APIListDetailView.as_view()(req, pk=new_list.pk)
# Asserts.
self.assertEqual(204, resp.status_code, "Should delete the list from database.")
self.assertEqual(current_list_amount, List.objects.count() - 1, "Should have delete a list from the database.")
如果您不熟悉测试,可能值得看看 factory boy 来模拟您的 Django 模型。
https://factoryboy.readthedocs.io/en/latest/
顺便说一下,您真的应该避免使用像 "List" 这样的通用词作为您的型号名称。
问题可能在于您如何制定 URL。您可以通过执行此操作直接反转 URL 以进行删除:
url = reverse('lists:Lists-detail', kwargs={'pk': new_list.pk})
self.client.delete(url).
使用这种方法,您将不会遇到忘记尾部斜杠或在不需要时添加斜杠等问题。
问题也可能出在您的视图集中,因为您使用的是自定义 ModelViewset,但您说它适用于 JS 客户端,所以这可能不是问题所在。
我正在为我的 Django Rest Framework 编写测试 API。
我一直在测试 'delete'。
我对 'create' 的测试工作正常。
这是我的测试代码:
import json
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from users.models import CustomUser
from lists.models import List, Item
class ListAPITest(APITestCase):
@classmethod
def setUp(self):
self.data = {'name': 'Test list', 'description':'A description', 'item': [
{'name': 'Item 1 Name', 'description': 'Item 1 description', 'order': 1},
{'name': 'Item 2 Name', 'description': 'Item 2 description', 'order': 2},
{'name': 'Item 3 Name', 'description': 'Item 3 description', 'order': 3},
{'name': 'Item 4 Name', 'description': 'Item 4 description', 'order': 4},
{'name': 'Item 5 Name', 'description': 'Item 5 description', 'order': 5},
{'name': 'Item 6 Name', 'description': 'Item 6 description', 'order': 6},
{'name': 'Item 7 Name', 'description': 'Item 7 description', 'order': 7},
{'name': 'Item 8 Name', 'description': 'Item 8 description', 'order': 8},
{'name': 'Item 9 Name', 'description': 'Item 9 description', 'order': 9},
{'name': 'Item 10 Name', 'description': 'Item 10 description', 'order': 10}
]}
# 'lists' is the app_name set in endpoints.py
# 'Lists' is the base_name set for the list route in endpoints.py
# '-list' seems to be something baked into the api
self.url = reverse('lists:Lists-list')
def test_create_list_authenticated(self):
"""
Ensure we can create a new list object.
"""
user = CustomUser.objects.create(email='person@example.com', username='Test user', email_verified=True)
self.client.force_authenticate(user=user)
response = self.client.post(self.url, self.data, format='json')
list_id = json.loads(response.content)['id']
# the request should succeed
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# there should now be 1 List in the database
self.assertEqual(List.objects.count(), 1)
def test_delete_list_by_owner(self):
"""
delete list should succeed if user created list
"""
user = CustomUser.objects.create(email='person@example.com', username='Test user', email_verified=True)
new_list = List.objects.create(name='Test list', description='A description', created_by=user, created_by_username=user.username)
self.client.force_authenticate(user=user)
response = self.client.delete(self.url + '/' + str(new_list.id))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
我看到的不是预期的状态 204:
AssertionError: 405 != 204
405 是不允许的方法。
这是我的模型定义:
class List(models.Model):
"""Models for lists
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_by = models.ForeignKey(USER, on_delete=models.CASCADE, related_name='list_created_by_id')
created_by_username = models.CharField(max_length=255) # this shold be OK given that the list will be deleted if the created_by_id user is deleted
created_at = models.DateTimeField(auto_now_add=True)
parent_item = models.ForeignKey('Item', on_delete=models.SET_NULL, null=True, related_name='parent_item')
modified_by = models.ForeignKey(USER, on_delete=models.SET_NULL, null=True,
related_name='list_modified_by')
modified_at = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=255)
description = models.CharField(max_length=5000, blank=True, default='')
is_public = models.BooleanField(default=False)
def __str__(self):
return self.name
这是我的观点:
class ListViewSet(FlexFieldsModelViewSet):
"""
ViewSet for lists.
"""
permission_classes = [IsOwnerOrReadOnly, HasVerifiedEmail]
model = List
serializer_class = ListSerializer
permit_list_expands = ['item']
pagination_class = LimitOffsetPagination
def get_queryset(self):
# unauthenticated user can only view public lists
queryset = List.objects.filter(is_public=True)
# authenticated user can view public lists and lists the user created
# listset in query parameters can be additional filter
if self.request.user.is_authenticated:
listset = self.request.query_params.get('listset', None)
if listset == 'my-lists':
queryset = List.objects.filter(created_by=self.request.user)
elif listset == 'public-lists':
queryset = List.objects.filter(is_public=True)
else:
queryset = List.objects.filter(
Q(created_by=self.request.user) |
Q(is_public=True)
)
# allow filter by URL parameter created_by
created_by = self.request.query_params.get('created_by', None)
if created_by is not None:
queryset = queryset.filter(created_by=created_by)
# return only lists that have no parent item
toplevel = self.request.query_params.get('toplevel')
if toplevel is not None:
queryset = queryset.filter(parent_item=None)
return queryset.order_by('name')
我已阅读 docs,但我无法找到如何设置删除请求。
我也试过这个:
kwargs = {'pk': new_list.id}
response = self.client.delete(self.url, **kwargs)
这给了我一个错误:
AssertionError: Expected view ListViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
通过我的 React 前端中的 API 在我的应用程序中删除工作正常。
我知道我的对象名为 List 令人困惑...但很难想到另一个名称,因为它就是这样!
感谢您提供我在这里遗漏的任何想法!
我建议你看看 Django-restframework 测试文档。
https://www.django-rest-framework.org/api-guide/testing/
这是我将如何针对您的当前情况编写测试的示例。
from rest_framework.test import APIRequestFactory, force_authenticate
from django.test import TestCase
class TestsAPIListDetailView(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
# This only matters if you are passing url query params e.g. ?foo=bar
self.baseUrl = "/list/"
def test_delete_with_standard_permission(self):
# Creates mock objects
user = CustomUser.objects.create(email='person@example.com', username='Test user', email_verified=True)
new_list = List.objects.create(name='Test list', description='A description', created_by=user,
created_by_username=user.username)
# Creates a mock delete request.
# The url isn't strictly needed here. Unless you are using query params e.g. ?q=bar
req = self.factory.delete("{}{}/?q=bar".format(self.baseUrl, new_list.pk))
current_list_amount = List.object.count()
# Authenticates the user with the request object.
force_authenticate(req, user=user)
# Returns the response data if you ran the view with request(e.g if you called a delete request).
# Also you can put your url kwargs(For example for /lists/<pk>/) like pk or slug in here. Theses kwargs will be automatically passed to view.
resp = APIListDetailView.as_view()(req, pk=new_list.pk)
# Asserts.
self.assertEqual(204, resp.status_code, "Should delete the list from database.")
self.assertEqual(current_list_amount, List.objects.count() - 1, "Should have delete a list from the database.")
如果您不熟悉测试,可能值得看看 factory boy 来模拟您的 Django 模型。 https://factoryboy.readthedocs.io/en/latest/
顺便说一下,您真的应该避免使用像 "List" 这样的通用词作为您的型号名称。
问题可能在于您如何制定 URL。您可以通过执行此操作直接反转 URL 以进行删除:
url = reverse('lists:Lists-detail', kwargs={'pk': new_list.pk})
self.client.delete(url).
使用这种方法,您将不会遇到忘记尾部斜杠或在不需要时添加斜杠等问题。 问题也可能出在您的视图集中,因为您使用的是自定义 ModelViewset,但您说它适用于 JS 客户端,所以这可能不是问题所在。