Ошибка утверждения при разборе ответа на POST-запрос из набора представлений DRF с помощью DRF APIClient в тестах

Я столкнулся со странной проблемой с движком тестирования Django Rest Framework. Странно то, что раньше все прекрасно работало на Django 3, а эта проблема появилась после того, как я перешел на Django 4. Помимо тестирования, все работает хорошо и отвечает на запросы, как и ожидалось.

Проблема

Я использую DRF APIClient для создания запросов для модульных тестов. В то время как GET-запросы выполняются предсказуемо, мне не удается заставить POST-запросы работать.

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

Python 3.9
Django==4.0.3
djangorestframework==3.13.1
from django.db import models
from django.urls import include, path
from django.utils import timezone
from rest_framework import routers, serializers, viewsets

router = routers.DefaultRouter()


# models.py

class SomeThing(models.Model):
    created_at = models.DateTimeField(default=timezone.now)
    title = models.CharField(max_length=100, null=True, blank=True)


# serializers.py

class SomeThingSerializer(serializers.ModelSerializer):
    class Meta:
        fields = "__all__"
        model = SomeThing


# views.py

class SomeThingViewSet(viewsets.ModelViewSet):
    queryset = SomeThing.objects.all().order_by('id')
    serializer_class = SomeThingSerializer


# urls.py

router.register("some-things", SomeThingViewSet, basename="some_thing")

app_name = 'question'
urlpatterns = (
    path('', include(router.urls)),
)

Вот мой тестовый пример:

import json

from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APITestCase, APIClient


class TestUserView(APITestCase):
        self.some_user = get_user_model().objects.create(login="some_user@test.ru")

    @staticmethod
    def get_client(user):
        client = APIClient()
        client.force_authenticate(user=user)
        return client


    def test_do_something(self):
        client = self.get_client(self.compliance_chief)
        url = reverse('question:some_things-list')
        resp = client.post(
            path=url,
            data=json.dumps({"title": "Created Something"}),
            content_type="application/json",
        )
        assert resp.status_code == status.HTTP_201_OK

(Да, я должен использовать некоторую аутентификацию для получения доступа к данным, но я не думаю, что это имеет отношение к проблеме). На что я получаю длинный трассировочный ответ, заканчивающийся ошибкой утверждения:

  File "/****/****/****/venv/lib/python3.9/site-packages/django/test/client.py", line 82, in read
    assert (
AssertionError: Cannot read more than the available bytes from the HTTP incoming data.

Поскольку она действительно довольно длинная, я оставлю ее на всякий случай в gist, не публикуя здесь.

Шаги по устранению

Проблема явно возникает после того, как набор представлений возвращает правильный ответ. Чтобы убедиться в правильности ответа, я немного изменил метод create, чтобы распечатать ответ до его возвращения, как показано ниже:

class SomeThingViewSet(viewsets.ModelViewSet):
    queryset = SomeThing.objects.all().order_by('id')
    serializer_class = SomeThingSerializer

    def create(self, request, *args, **kwargs):
        response = super().create(request, *args, **kwargs)
        print("THIS IS THE RESPONSE FROM THE VIEWSET", response)
        return response

И, конечно, результат правильный:

THIS IS THE RESPONSE FROM THE VIEWSET <Response status_code=201, "text/html; charset=utf-8">

Что заставляет меня думать, что что-то идет не так на этапе парсинга (фактически, трассировка подразумевает то же самое). Я попытался изменить способ построения запроса, а именно:

  • используя формат вместо типа содержимого, например: resp = client.post(path=url, data={"title": "Created Something"}, format="json")
  • использование метода .generic вместо .post, как показано ниже: resp = client.generic(method="POST", path=url, data=json.dumps({"title": "Created Something"}), content_type="application/json")

Результат тот же.

Погуглив, я обнаружил, что эта ошибка действительно иногда возникала в связи с DRF APIClient и Django, но очень давно (например, это обсуждение, которое утверждает, что проблема была исправлена в более поздних версиях Django)

Я уверен, что причина такого поведения достаточно очевидна (скорее всего, какая-то глупая ошибка), и решение должно быть очень простым, но пока мне не удалось его найти. Я был бы очень признателен, если бы кто-нибудь поделился своим опытом, если таковой имеется, в решении подобной проблемы, или своими соображениями о том, как выйти из этого тупика.

Итак, загадка решена, и я собираюсь поделиться ею здесь на случай, если кто-то столкнется с чем-то подобным, хотя для этого потребуется довольно большое совпадение, так что это маловероятно.

Краткая история: я испортил исходный код моего Django4.0.3. установленного в этом проекте.

Теперь о том, как это произошло. Когда я тестировал некоторые вещи, я столкнулся с ошибкой, которую не смог обнаружить, поэтому я пошел по всей цепочке событий, проверяя, был ли вывод тем, что я ожидал. Вскоре я обнаружил, что проверяю вывод функций из библиотек, установленных в моей виртуальной среде. Я понимаю, что напрямую изменять их код - это плохая практика, но поскольку я работал в своей локальной среде с возможностью переустановить все в любой момент, я решил, что можно поиграть с ними. Поскольку это ни к чему не привело, я удалил весь код, который я добавил (или так я думал).

Через некоторое время я понял, что вызвало первоначальную ошибку (упущенное условие в моей тестовой установке), исправил ее и попытался запустить тест. Тогда-то и проявилась рассматриваемая проблема.

Позже я обнаружил, что тот же самый тест работает правильно в идентичном окружении. Тогда я заподозрил, что что-то сломал в коде своей локальной библиотеки. Далее я просто сравнил код, с которым я имел дело в локальном окружении, с кодом из официального источника и довольно скоро обнаружил нарушающую строку. Она оказалась в django/test/client.py, в определении метода RequestFactory.generic. Что-то вроде этого:

...
        if not r.get("QUERY_STRING"):
            # WSGI requires latin-1 encoded strings. See get_path_info().
            query_string = parsed[4].encode().decode("iso-8859-1")
            r["QUERY_STRING"] = query_string
        req = self.request(**r)
        return self.request(**r)
...

Преступной строкой (которую я добавил и забыл удалить) была req = self.request(**r). После того как я удалил ее, все вернулось в норму.

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