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

Tor*_*sen 43 django django-unittest django-rest-framework

我有一个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.'})
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

但是当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'>)
Run Code Online (Sandbox Code Playgroud)

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

Kev*_*own 31

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

@arocks的评论中指出了这一点

通过{'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)
Run Code Online (Sandbox Code Playgroud)

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

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

这也提出了类似的问题,@ RobinElvin在评论中指出了这一点

那是因为我缺少格式='multipart'

  • 由于某种原因,这导致我400错误.返回的错误是{"file":["提交的文件为空."]}. (13认同)
  • 不得不在`post`之前添加`tmp_file.seek(0)`,否则完美!这几乎让我疯了如此谢谢! (4认同)
  • 请注意,如果您不想因任何原因(perf将是一个)分配临时文件,您还可以执行`tmp_file = BytesIO(b'some text')`; 这将为您提供一个可以作为文件对象传递的二进制流.(https://docs.python.org/3/library/io.html). (3认同)

Mei*_*tro 15

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')
Run Code Online (Sandbox Code Playgroud)

  • 我认为`{'image':data}`应该是答案中的`{'image':fp}`.我一直在努力上传,直到找到这篇文章,但直到我把文件句柄对象`fp`代替上面描述的`{'image':data}`字典中的`data`,我的测试才通过.(`{'image':fp}`在我的情况下工作,`{'image':data}`没有.) (3认同)

Igo*_*gor 8

您可以使用 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)

Run Code Online (Sandbox Code Playgroud)


Ant*_*hov 6

如果你想使用 PATCH 方法,理解如何去做并不是那么简单,但我在这个问题中找到了解决方案。

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
    )
Run Code Online (Sandbox Code Playgroud)