Ошибка утверждения при разборе ответа на 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)
. После того как я удалил ее, все вернулось в норму.