Как написать тест для операции «удалить» в среде Django rest

Я пишу тесты для своего 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... но трудно придумать другое имя, потому что это именно так!

Спасибо за любые идеи, что мне здесь не хватает!

Можете ли вы добавить завершающая косая черта в конце вашего URL-адреса и посмотреть, изменит ли это что-нибудь?

mehamasum 10.03.2019 22:07

Спасибо, попробовал, ничего не изменилось.

Little Brain 11.03.2019 14:42

Вы можете добавить соответствующий вид?

JPG 16.03.2019 04:38

Я добавил ListViewSet в свой вопрос.

Little Brain 16.03.2019 04:58
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
4
4
7 472
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я рекомендую вам ознакомиться с документацией по тестированию 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 в запрос? Я упускаю что-то очевидное?

Little Brain 11.03.2019 20:13

@LittleBrain Извините, я копировал тест представления списка и забыл добавить kwargs URL для тестирования. Однако сейчас в это внесены поправки.

James Brewer 12.03.2019 10:36

Я хотел бы, чтобы мой код, основанный на примерах, работал, а не переписывал все подряд, и я хочу понять, что я делаю неправильно с self.client.delete. Не могли бы вы объяснить, как я буду указывать идентификатор в своем коде: response = self.client.delete(self.url)? Это должно быть просто, но я не могу найти документацию.

Little Brain 15.03.2019 20:56

Я не уверен, что здесь нужно использовать factory, похоже, что self.client является более полным тестом: reddit.com/r/django/comments/56ux4c/testcase_vs_requestfacto‌​ry. Я должен быть почти готов, потому что тест создания работает, мне нужно только знать, как создать запрос на удаление.

Little Brain 16.03.2019 02:56
Ответ принят как подходящий

Проблема может заключаться в том, как вы формулируете URL-адрес. Вы можете изменить URL-адрес для удаления напрямую, выполнив следующие действия:

 url = reverse('lists:Lists-detail', kwargs = {'pk': new_list.pk})
 self.client.delete(url). 

При таком подходе у вас не будет таких проблем, как забывание косой черты в конце или добавление ее, когда она не нужна. Проблема также может быть в вашем наборе представлений, поскольку вы используете собственный ModelViewset, но вы сказали, что он работает с клиентом JS, поэтому проблема может быть не в этом.

Спасибо! Я не понял, что мне нужно было использовать «удалить» в обратном порядке или что обратное место было местом для ввода pk. Это работает :)

Little Brain 17.03.2019 09:49

@LittleBrain всегда пожалуйста. URL-адрес предназначен не для удаления, а для подробного маршрута, на котором работают методы PUT, PATCH и DELETE, в то время как POST и GET работают на маршруте списка.

Ken4scholars 17.03.2019 12:31

Спасибо, да, я должен был написать «подробно» в своем комментарии выше, а не «удалить».

Little Brain 17.03.2019 13:01

Другие вопросы по теме