Я пишу тесты для своего API Django Rest Framework.
Я застрял на тестировании «удалить».
Мой тест на «создать» работает нормально.
Вот мой тестовый код:
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='[email protected]', 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='[email protected]', 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')
Я прочитал документы, но не смог найти, как настроить запрос на удаление.
Я также пробовал это:
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.
Удаление в моем приложении отлично работает через API в моем интерфейсе React.
Я знаю, меня сбивает с толку то, что мой объект называется List... но трудно придумать другое имя, потому что это именно так!
Спасибо за любые идеи, что мне здесь не хватает!
Спасибо, попробовал, ничего не изменилось.
Вы можете добавить соответствующий вид?
Я добавил ListViewSet в свой вопрос.






Я рекомендую вам ознакомиться с документацией по тестированию 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='[email protected]', 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.")
Если вы новичок в тестировании, возможно, стоит взглянуть на фабричного мальчика, который издевается над вашими моделями Django. https://factoryboy.readthedocs.io/en/latest/
Кстати, вам действительно следует избегать использования общих слов, таких как «Список», для названий ваших моделей.
Привет, Джеймс, спасибо, но разве тебе не нужно передавать идентификатор new_list в запрос? Я упускаю что-то очевидное?
@LittleBrain Извините, я копировал тест представления списка и забыл добавить kwargs URL для тестирования. Однако сейчас в это внесены поправки.
Я хотел бы, чтобы мой код, основанный на примерах, работал, а не переписывал все подряд, и я хочу понять, что я делаю неправильно с self.client.delete. Не могли бы вы объяснить, как я буду указывать идентификатор в своем коде: response = self.client.delete(self.url)? Это должно быть просто, но я не могу найти документацию.
Я не уверен, что здесь нужно использовать factory, похоже, что self.client является более полным тестом: reddit.com/r/django/comments/56ux4c/testcase_vs_requestfactory. Я должен быть почти готов, потому что тест создания работает, мне нужно только знать, как создать запрос на удаление.
Проблема может заключаться в том, как вы формулируете URL-адрес. Вы можете изменить URL-адрес для удаления напрямую, выполнив следующие действия:
url = reverse('lists:Lists-detail', kwargs = {'pk': new_list.pk})
self.client.delete(url).
При таком подходе у вас не будет таких проблем, как забывание косой черты в конце или добавление ее, когда она не нужна. Проблема также может быть в вашем наборе представлений, поскольку вы используете собственный ModelViewset, но вы сказали, что он работает с клиентом JS, поэтому проблема может быть не в этом.
Спасибо! Я не понял, что мне нужно было использовать «удалить» в обратном порядке или что обратное место было местом для ввода pk. Это работает :)
@LittleBrain всегда пожалуйста. URL-адрес предназначен не для удаления, а для подробного маршрута, на котором работают методы PUT, PATCH и DELETE, в то время как POST и GET работают на маршруте списка.
Спасибо, да, я должен был написать «подробно» в своем комментарии выше, а не «удалить».
Можете ли вы добавить завершающая косая черта в конце вашего URL-адреса и посмотреть, изменит ли это что-нибудь?