Тесты Django views возвращают код 403/200 вместо (предположительно) 302

Я тестирую представления в своем приложении на Django. Поскольку приложение является серверной частью сайта форума, я пытаюсь протестировать создание, редактирование и удаление темы.

Создание, редактирование и удаление темы реализовано в моем приложении для работы с перенаправлением:

  • страница создания (Добавить тему) перенаправляет на страницу успешно созданной темы;
  • редактирование первоначального комментария к теме (UpdateFirstComment) приводит к перенаправлению со страницы редактирования на страницу редактируемой темы;
  • страница удаления (DeleteTopic) перенаправляет на подфорум (раздел форума), к которому принадлежала удаленная тема.

Я предполагаю (я не уверен; и, скорее всего, это моя ошибка), что код успешного перенаправления равен 302, и в утверждении тестов именно этот код следует проверить. Но на практике тесты на создание и редактирование возвращают код 200, в то время как тест на удаление возвращает код 403. И я, из-за отсутствия опыта, вряд ли смогу объяснить, почему это происходит именно так и как с этим бороться.

views.py:

forum/urls.py (только для подфорумов и тем):

<...>
app_name = 'forum'

urlpatterns = [
    path('', SubForumListView.as_view(), name='forum'),
    path('<slug:subforum_slug>/', TopicListView.as_view(), name='subforum'),
    path('<slug:subforum_slug>/add_topic/', AddTopic.as_view(), name="add_topic"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/edit_topic/', UpdateFirstComment.as_view(), name='edit_topic'),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/delete_topic/', DeleteTopic.as_view(), name='delete_topic'),
]

tests.py (тесты в разделе комментариев опущены, они работают нормально):

from django.test import TestCase
from django.urls import reverse

from . import factories, models
from .models import Topic, Comment


class SubforumTestCase(TestCase):
    def setUp(self):
        self.subforum = factories.SubForumFactory()
        self.user = factories.UserFactory()
        self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
        self.client.force_login(self.user)

    def test_get_topic_list(self):
        url = reverse('forum:subforum', kwargs={'subforum_slug': self.subforum.slug})
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['topics'].count(), models.Topic.objects.count())

    def test_get_topic_detail(self):
        url = reverse("forum:topic", kwargs={'subforum_slug': self.subforum.slug, 'topic_slug': self.topic.slug})
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, "forum/topic.html")

    def test_add_topic(self):
        data = {
            'subject': self.topic.subject,
            'first_comment': self.topic.first_comment
        }
        url = reverse("forum:add_topic", kwargs={'subforum_slug': self.subforum.slug})
        old_topics_count = Topic.objects.count()
        response = self.client.post(url, data=data)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(Topic.objects.count(), 2)
        self.assertGreater(Topic.objects.count(), old_topics_count)

    def test_update_first_comment(self):
        data = {
            'first_comment': "Chebuldyk"
        }
        url = reverse("forum:edit_topic", kwargs={
            'subforum_slug': self.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_first_comment = self.topic.first_comment
        response = self.client.post(url, data=data)
        self.topic.refresh_from_db()

        self.assertEqual(response.status_code, 302)
        self.assertNotEqual(self.topic.first_comment, old_first_comment)

    def test_delete_topic(self):
        url = reverse("forum:delete_topic", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_topics_count = Topic.objects.count()
        response = self.client.delete(url)

        self.assertEqual(response.status_code, 302)
        self.assertGreater(old_topics_count, Topic.objects.count())

результаты тестирования:

Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...FF..F
======================================================================
FAIL: test_add_topic (forum.tests.SubforumTestCase.test_add_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 38, in test_add_topic
    self.assertEqual(response.status_code, 302)
AssertionError: 200 != 302

======================================================================
FAIL: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\...\forum\tests.py", line 65, in test_delete_topic
    self.assertEqual(response.status_code, 302)
AssertionError: 403 != 302

======================================================================
FAIL: test_update_first_comment (forum.tests.SubforumTestCase.test_update_first_comment)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 54, in test_update_first_comment
    self.assertEqual(response.status_code, 302)
AssertionError: 200 != 302

----------------------------------------------------------------------
Ran 8 tests in 0.111s

FAILED (failures=3)

Тесты работают на заводах-изготовителях; при необходимости я могу предоставить и их.

Значение response.status.code для функций добавления и обновления, равное 200, а не 301, указывает на то, что вы получаете ошибки, поэтому вы не переходите к success_url, а остаетесь на странице, на которую отправляете сообщение.

Попробуйте добавить функцию form_invalid для вывода ошибок на ваш терминал

def form_invalid(self, form):
        response = super().form_invalid(form)
        print form.errors

Ошибка 403 для delete_topic связана с тем, что вы используете self.client.delete. Хотя DELETE является допустимым типом HTTP-запроса, он обычно не используется для взаимодействия с веб-страницами. Скорее всего, вам понадобится self.client.get (для простого URL-адреса или со строкой запроса) или self.client.post (если вы публикуете данные формы) для связи с представлением, которое выполняет фактическое удаление экземпляра модели с помощью кода.

Я пытался удалить этот вопрос, так как получил помощь с ним на другом сайте, но он мне не позволяет, поэтому я опубликую ответ, чтобы он был полезен другим:

  • тест_адд_топик

print(response.context["form"].errors) предыдущие проверки показали, что проблема в том, что тема создается с уже существующей темой. Причина в том, что тест на создание темы получил тему из уже созданной темы-подделки.

Решение: вставить какую-нибудь оригинальную тему для созданной темы:

def test_add_topic(self):
    data = {
        'subject': "Kai Cenat fanum tax",  # here
        'first_comment': self.topic.first_comment
    }
    url = reverse("forum:add_topic", kwargs={'subforum_slug': self.subforum.slug})
    old_topics_count = Topic.objects.count()
    response = self.client.post(url, data=data)

    #print(response.context["form"].errors)
    self.assertRedirects(
        response,
        f"/forum/{self.topic.subforum.slug}/topics/topic-{slugify(data['subject'])}/",
        status_code=302,
        target_status_code=200
    )
    self.assertEqual(response.status_code, 302)
    self.assertEqual(Topic.objects.count(), 2)
    self.assertGreater(Topic.objects.count(), old_topics_count)
  • тестовое обновление_фирст_комментария

Моя ошибка. Поскольку тема является уникальным и запрашиваемым полем для темы, она должна была быть указана в data.

Решение:

def test_update_first_comment(self):
    data = {
        'subject': self.topic.subject,  # here
        'first_comment': "Chebuldyk"
    }
    url = reverse("forum:edit_topic", kwargs={
        'subforum_slug': self.subforum.slug,
        'topic_slug': self.topic.slug
    })
    old_first_comment = self.topic.first_comment
    response = self.client.post(url, data=data)
    self.topic.refresh_from_db()

    # print(response.context["form"].errors)
    self.assertRedirects(
        response,
        f"/forum/{self.topic.subforum.slug}/topics/{self.topic.slug}/",
        status_code=302,
        target_status_code=200
    )
    self.assertEqual(response.status_code, 302)
    self.assertNotEqual(self.topic.first_comment, old_first_comment)
  • test_delete_topic

Из-за введенных мной ограничений для удаления темы требуются права администратора. Поэтому для настройки тестов потребовалось добавить другого пользователя с типом администратора. Соответствующий фейкер был создан в factories.py.

После этого возникла проблема с AttributeError: Generic detail view DeleteTopic must be called with either an object pk or a slug in the URLconf., которая на этот раз была связана с соответствующим представлением. DeleteView должен либо содержать slug_url_kwarg определенный атрибут, либо метод get_object перезаписано, чтобы указать, к какому URL обращаться.

Решение:

factories.py:

class UserFactory(factory.django.DjangoModelFactory):
    username = factory.Faker('user_name')
    password = factory.Faker('password')
    email = factory.Faker('email')

    class Meta:
        model = User


class AdminFactory(UserFactory):
    class Params:
        superuser = factory.Trait(is_superuser=True, is_staff=True)

views.py Удалить просмотр темы:

class DeleteTopic(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Topic
    context_object_name = 'topic'
    slug_url_kwarg = 'topic_slug'  # here
    template_name = 'forum/topic_confirm_delete.html'
    page_title = "Delete topic"
    fields = '__all__'

    def test_func(self):
        if self.request.user.is_superuser:
            return True
        return False

    ''' 
    # or here; but in this separate case that's redundant
    # Necessary when there are several parameters to make clear which slug to address
    def get_object(self, queryset=None):
        return Topic.objects.get(slug=self.kwargs['topic_slug'], subforum__slug=self.kwargs['subforum_slug'])
    '''

    def get_success_url(self):
        return reverse('forum:subforum', kwargs={'subforum_slug': self.kwargs['subforum_slug']})

tests.py Настройка и тест_delete_topic:

def setUp(self):
    self.subforum = factories.SubForumFactory()
    self.user = factories.UserFactory()
    self.super_admin = factories.AdminFactory(superuser=True)  # here
    self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
    # self.client.force_login(self.user)
    self.client.force_login(self.super_admin)  # here

def test_delete_topic(self):
    url = reverse("forum:delete_topic", kwargs={
        'subforum_slug': self.subforum.slug,
        'topic_slug': self.topic.slug
    })
    old_topics_count = Topic.objects.count()
    response = self.client.delete(url)

    # print(response)
    # print(response.context["form"].errors)
    self.assertRedirects(response, f"/forum/{self.subforum.slug}/", status_code=302, target_status_code=200)
    self.assertEqual(response.status_code, 302)
    self.assertGreater(old_topics_count, Topic.objects.count())

Приветствую всех на форуме Django. Ребята - спасители.

Вернуться на верх