如何使用 django-rest-framework 测试客户端测试二进制文件上传?

Posted

技术标签:

【中文标题】如何使用 django-rest-framework 测试客户端测试二进制文件上传?【英文标题】:How can I test binary file uploading with django-rest-framework's test client? 【发布时间】:2014-08-03 19:09:15 【问题描述】:

我有一个 Django 应用程序,其视图接受要上传的文件。使用 Django REST 框架,我将 APIView 子类化并实现 post() 方法,如下所示:

class FileUpload(APIView):
    permission_classes = (IsAuthenticated,)

    def post(self, request, *args, **kwargs):
        try:
            image = request.FILES['image']
            # Image processing here.
            return Response(status=status.HTTP_201_CREATED)
        except KeyError:
            return Response(status=status.HTTP_400_BAD_REQUEST, data='detail' : 'Expected image.')

现在我正在尝试编写几个单元测试以确保需要身份验证并且实际处理上传的文件。

class TestFileUpload(APITestCase):
    def test_that_authentication_is_required(self):
        self.assertEqual(self.client.post('my_url').status_code, status.HTTP_401_UNAUTHORIZED)

    def test_file_is_accepted(self):
        self.client.force_authenticate(self.user)
        image = Image.new('RGB', (100, 100))
        tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg')
        image.save(tmp_file)
        with open(tmp_file.name, 'rb') as data:
            response = self.client.post('my_url', 'image': data, format='multipart')
            self.assertEqual(status.HTTP_201_CREATED, response.status_code)

但是当 REST 框架尝试对请求进行编码时,这会失败

Traceback (most recent call last):
  File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/django/utils/encoding.py", line 104, in force_text
    s = six.text_type(s, encoding, errors)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 118: invalid start byte

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/vagrant/webapp/myproject/myapp/tests.py", line 31, in test_that_jpeg_image_is_accepted
    response = self.client.post('my_url',  'image': data, format='multipart')
  File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-    packages/rest_framework/test.py", line 76, in post
    return self.generic('POST', path, data, content_type, **extra)
  File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/rest_framework/compat.py", line 470, in generic
    data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
  File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/django/utils/encoding.py", line 73, in smart_text
    return force_text(s, encoding, strings_only, errors)
  File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/django/utils/encoding.py", line 116, in force_text
    raise DjangoUnicodeDecodeError(s, *e.args)
django.utils.encoding.DjangoUnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 118: invalid start byte. You passed in b'--BoUnDaRyStRiNg\r\nContent-Disposition: form-data; name="image"; filename="tmpyz2wac.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\xff\xd8\xff[binary data omitted]' (<class 'bytes'>)

如何让测试客户端发送数据而不尝试将其解码为 UTF-8?

【问题讨论】:

改为传递 'image': file @arocks 鹰眼!我已经更正了帖子中的错字,实际代码没有这个问题。 您的代码对我有用。谢谢! 我也有同样的问题。你解决了吗? 那是因为我错过了 format='multipart' - doh 【参考方案1】:

在测试文件上传时,您应该将流对象传递给请求,而不是数据

@arocks 在 cmets 中指出了这一点

改为传递 'image': file

但这并没有完全解释为什么需要它(也与问题不匹配)。对于这个特定的问题,您应该这样做

from PIL import Image

class TestFileUpload(APITestCase):

    def test_file_is_accepted(self):
        self.client.force_authenticate(self.user)

        image = Image.new('RGB', (100, 100))

        tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg')
        image.save(tmp_file)
        tmp_file.seek(0)

        response = self.client.post('my_url', 'image': tmp_file, format='multipart')

       self.assertEqual(status.HTTP_201_CREATED, response.status_code)

这将匹配标准 Django 请求,其中文件作为流对象传入,由 Django REST Framework 处理。当您只传入文件数据时,Django 和 Django REST Framework 将其解释为字符串,这会导致问题,因为它需要一个流。

对于那些来这里寻找另一个常见错误的人,为什么文件上传不起作用但正常的表单数据会:确保在创建请求时设置format="multipart"

这也给出了类似的问题,并在 cmets 中被 @RobinElvin 指出

这是因为我缺少 format='multipart'

【讨论】:

由于某种原因,这导致我出现 400 错误。返回的错误是 "file":["提交的文件为空。"]。 请注意,如果您出于任何原因不想分配临时文件(perf 将是一个),您也可以使用tmp_file = BytesIO(b'some text');这将为您提供一个可以作为文件对象传递的二进制流。 (docs.python.org/3/library/io.html)。 必须在post 之前添加tmp_file.seek(0),但其他方面完美!这几乎把我逼疯了,所以谢谢! Image 模块来自哪里? @RudolfOlah Image 来自 Pillow 库。见pillow.readthedocs.io/en/stable。【参考方案2】:

Python 3 用户:确保您 open mode='rb' 中的文件(读取,二进制)。否则,当 Django 在文件上调用 read 时,utf-8 编解码器将立即开始阻塞。该文件应解码为二进制而不是 utf-8、ascii 或任何其他编码。

# This won't work in Python 3
with open(tmp_file.name) as fp:
        response = self.client.post('my_url', 
                                   'image': fp, 
                                   format='multipart')

# Set the mode to binary and read so it can be decoded as binary
with open(tmp_file.name, 'rb') as fp:
        response = self.client.post('my_url', 
                                   'image': fp, 
                                   format='multipart')

【讨论】:

我认为答案中的'image': data 应该是'image': fp。在找到这篇文章之前,我一直在努力上传,但直到我将文件句柄对象fp 代替data 放在上述'image': data 字典中,我的测试才通过。 ('image': fp 对我有用,'image': data 没有。) 已更新。谢谢 DMfll。【参考方案3】:

你可以使用 Django 内置 SimpleUploadedFile:

from django.core.files.uploadedfile import SimpleUploadedFile

class TestFileUpload(APITestCase):
...

    def test_file_is_accepted(self):
        ...

       tmp_file = SimpleUploadedFile(
                      "file.jpg", "file_content", content_type="image/jpg")

       response = self.client.post(
                      'my_url', 'image': tmp_file, format='multipart')
       self.assertEqual(response.status_code, status.HTTP_201_CREATED)

【讨论】:

【参考方案4】:

如果要使用PATCH方法,理解起来并不简单,但是我在this question找到了解决方案。

from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart

with open(tmp_file.name, 'rb') as fp:
    response = self.client.patch(
        'my_url', 
        encode_multipart(BOUNDARY, 'image': fp), 
        content_type=MULTIPART_CONTENT
    )

【讨论】:

【参考方案5】:

对于那些在 Windows 中的用户,答案有点不同。我必须执行以下操作:

resp = None
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp_file:
    image = Image.new('RGB', (100, 100), "#ddd")
    image.save(tmp_file, format="JPEG")
    tmp_file.close()

# create status update
with open(tmp_file.name, 'rb') as photo:
    resp = self.client.post('/api/articles/', 'title': 'title',
                                               'content': 'content',
                                               'photo': photo,
                                               , format='multipart')
os.remove(tmp_file.name)

区别,正如这个答案 (https://***.com/a/23212515/72350) 中所指出的,该文件在 Windows 中关闭后无法使用。在 Linux 下,@Meistro 的答案应该可以工作。

【讨论】:

在 OSX10.12.5 我得到了ValueError: I/O operation on closed file 我认为您不需要 tmp_file.close()with 声明。

以上是关于如何使用 django-rest-framework 测试客户端测试二进制文件上传?的主要内容,如果未能解决你的问题,请参考以下文章

文本标注系统

为啥 django-rest-framework 不显示 OneToOneField 数据 - django

尽管有 AllowAny 权限,django-rest-framework 在 POST、PUT、DELETE 上返回 403 响应

[精选] Mysql分表与分库如何拆分,如何设计,如何使用

如果加入条件,我该如何解决。如果使用字符串连接,我如何使用

如何使用本机反应创建登录以及如何验证会话