简体   繁体   English

在测试中使用 DRF APIClient 解析来自 DRF 视图集的 POST 请求的断言错误

[英]Assertion error parsing response to POST request from DRF viewset with DRF APIClient in tests

I've run into a strange issue with Django Rest Framework testing engine.我在使用 Django Rest 框架测试引擎时遇到了一个奇怪的问题。 The weird thing is that everything used to work fine with Django 3 and this issue turned up after I migrated to Django 4. Apart from testing, everything works well, and responds to queries as expected.奇怪的是,以前在 Django 3 上一切正常,而在我迁移到 Django 4 后出现了这个问题。除了测试之外,一切正常,并按预期响应查询。

The problem问题

I'm using DRF APIClient to make queries for unit tests.我正在使用 DRF APIClient 对单元测试进行查询。 While GET requests perform predictably, I fail to make POST requests work.虽然 GET 请求可以按预期执行,但我无法使 POST 请求正常工作。

Here is some minimalistic example code I created to figure out the issue.这是我为解决问题而创建的一些简约示例代码。 The versions I'm using:我正在使用的版本:

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)),
)

Here is my test case:这是我的测试用例:

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

(Yes, I have to use some authentication to get access to the data, but I don't think it is relevant to the problem.) To which I receive a lengthy traceback, ending with an assertion error: (是的,我必须使用一些身份验证来访问数据,但我认为这与问题无关。)我收到了一个冗长的回溯,以断言错误结束:

  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.

As it is really fairly long, I'll leave it just in case in a gist without posting it here.由于它真的很长,我会留下它以防万一,而不在这里发布。

Steps to fix修复步骤

The problem clearly happens after the correct response is returned by the viewset.在视图集返回正确的响应后,问题显然发生了。 To make sure the response is correct I made a slight customisation in the create method to print out the response before it is returned, like so:为了确保响应正确,我在 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

And, sure enough, the result is correct:而且,果然,结果是正确的:

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

Which makes me think something goes wrong at the parsing stage (actually, the traceback implies the same).这让我觉得在解析阶段出了点问题(实际上,回溯意味着相同)。 I tried to tweak the way I build the query, namely:我试图调整构建查询的方式,即:

  • using format instead of content type like so: resp = client.post(path=url, data={"title": "Created Something"}, format="json")使用格式而不是内容类型,如下所示: resp = client.post(path=url, data={"title": "Created Something"}, format="json")
  • using the.generic method instead of.post like so: resp = client.generic(method="POST", path=url, data=json.dumps({"title": "Created Something"}), content_type="application/json")使用 .generic 方法而不是 .post 像这样: resp = client.generic(method="POST", path=url, data=json.dumps({"title": "Created Something"}), content_type="application/json")

The result is the same.结果是一样的。

From googling I found out that this error indeed has occasionally occurred in connection with DRF APIClient and Django, but really long ago (likethis discussion , which claims that the issue was fixed in the later versions of Django).通过谷歌搜索,我发现这个错误确实偶尔发生在与 DRF APIClient 和 Django 相关的情况下,但很久以前(就像这个讨论,它声称该问题已在更高版本的 Django 中得到修复)。

I'm sure the reason for this behaviour is rather obvious (some stupid mistake most likely) and the solution must be very simple, but so far I've failed to find it.我确信这种行为的原因是相当明显的(最有可能是一些愚蠢的错误)并且解决方案必须非常简单,但到目前为止我还没有找到它。 I would be very grateful if somebody shared their experience, if there is any, of dealing with such an issue, or their considerations as to where to move from this deadlock.如果有人分享他们处理此类问题的经验(如果有的话),或者他们对如何摆脱这种僵局的考虑,我将不胜感激。

Alright, the mystery's been resolved and I'm going to share it here in case somebody runs into something similar, although it would take quite a coincidence, so it is unlikely.好吧,这个谜已经解决了,我将在这里分享它,以防有人遇到类似的事情,虽然这需要相当巧合,所以不太可能。

Long story short: I messed up the source code of my Django4.0.3.长话短说:我弄乱了我的 Django4.0.3 的源代码。 installed in this project.安装在这个项目中。

Now, how it happened.现在,它是如何发生的。 While I was testing some stuff, I ran into an error, which I failed to locate, so I went along the whole chain of events checking if the output was what I expected it to be.当我测试一些东西时,我遇到了一个错误,我没有找到,所以我沿着整个事件链检查 output 是否符合我的预期。 Soon enough I found myself checking the output from functions in the libraries installed under my virtual environment.很快,我发现自己从安装在我的虚拟环境下的库中的函数中检查了 output。 I realise it's a malpractice to directly modify their code, but as I was working in my local environment with an option to reinstall everything at any moment, I decided it was fine to play with them.我意识到直接修改他们的代码是一种弊端,但是当我在本地环境中工作时可以选择随时重新安装所有内容时,我认为可以与他们一起玩。 As it resulted in nothing, I removed all the code I had added (or so I thought).由于没有任何结果,我删除了我添加的所有代码(或者我认为是这样)。

After a while I realised what caused the initial error (an overlooked condition in my testing setup), fixed it and tried to run the test.过了一会儿,我意识到是什么导致了初始错误(我的测试设置中被忽略的情况),修复了它并尝试运行测试。 That's when the problem in question showed up.这时候问题就出现了。

Later I found out that the same very test performs correctly in an identical environment.后来我发现同样的测试在相同的环境中也能正确执行。 Then I suspected that I broke something in my local library code.然后我怀疑我破坏了本地库代码中的某些内容。 Next I simply compared the code I had dealt with in my local environment with the code from the official source and soon enough I established the offending line.接下来,我只是将我在本地环境中处理的代码与官方来源的代码进行了比较,很快我就建立了违规行。 It happened to be in django/test/client.py, in the definition of the RequestFactory.generic method.它恰好在 django/test/client.py 中,在RequestFactory.generic方法的定义中。 Something like this:像这样的东西:

...
        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)
...

The offending line (which I added and forgot to remove) was req = self.request(**r) .违规行(我添加并忘记删除)是req = self.request(**r) After I deleted it, everything returned back to normal.删除后,一切恢复正常。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM