Как я могу протестировать загрузку двоичных файлов с помощью тестового клиента 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.'})

Теперь я пытаюсь написать пару модульных тестов, чтобы убедиться, что требуется аутентификация и что загруженный файл действительно обрабатывается.

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?


person Tore Olsen    schedule 13.06.2014    source источник
comment
Вместо этого передать { 'image': file}   -  person arocks    schedule 13.06.2014
comment
@arocks Орлиные глаза! Я исправил опечатку в сообщении, в реальном коде этой проблемы не было.   -  person Tore Olsen    schedule 13.06.2014
comment
Ваш код работает для меня. Благодарю вас!   -  person Khoi    schedule 16.07.2014
comment
У меня точно такая же проблема. Вы решили это?   -  person Robin Elvin    schedule 07.10.2014
comment
Это было потому, что мне не хватало format='multipart' - doh   -  person Robin Elvin    schedule 07.10.2014
comment
откуда берется временный файл?   -  person Adil Malik    schedule 11.05.2016


Ответы (5)


При тестировании загрузки файлов вы должны передавать в запрос объект потока, а не данные.

На это указал в комментариях @arocks.

Вместо этого передать { 'image': файл}

Но это не полностью объясняло, зачем это нужно (а также не соответствовало вопросу). Для этого конкретного вопроса вы должны делать

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" при создании запроса.

Это также вызывает аналогичную проблему, и на это указал @RobinElvin в комментариях.

Это было потому, что мне не хватало format='multipart'

person Kevin Brown    schedule 20.12.2014
comment
По какой-то причине это приводит меня к ошибке 400. Возвращенная ошибка: {file:[Отправленный файл пуст.]}. - person Divick; 04.02.2016
comment
Обратите внимание, что если вы по какой-либо причине не хотите выделять временный файл (одной из них будет perf), вы также можете выполнить tmp_file = BytesIO(b'some text'); это даст вам двоичный поток, который можно передать как файловый объект. (docs.python.org/3/library/io.html). - person Symmetric; 22.04.2016
comment
Пришлось добавить tmp_file.seek(0) перед post, а в остальном отлично! Это чуть не свело меня с ума, так что спасибо! - person jaywink; 01.08.2017
comment
откуда модуль Image? - person ; 10.02.2019
comment
@RudolfOlah Image происходит из библиотеки Pillow. См. pillow.readthedocs.io/en/stable. - person Clinton Blackburn; 06.07.2019
comment
Важно обратить внимание, что это format=multipart, а не content-type. Я оставлю это здесь, если кто-то также совершает ту же ошибку и получает 415 - person nck; 11.06.2021

Пользователи 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')
person Meistro    schedule 17.05.2015
comment
Я думаю, что {'image': data} должно быть {'image': fp} в ответе. Я боролся с загрузками, пока не нашел этот пост, но мои тесты не прошли, пока я не поместил объект дескриптора файла fp вместо data в словарь {'image': data}, описанный выше. (в моем случае {'image': fp} сработало, а {'image': data} нет.) - person dmmfll; 17.08.2015
comment
Обновлено. Спасибо DMfll. - person Meistro; 10.01.2017

Вы можете использовать встроенный Django Простой загруженный файл:

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)

person Igor    schedule 01.11.2019

Не так просто понять, как это сделать, если вы хотите использовать метод 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
    )
person Anton Shurashov    schedule 01.06.2018

Для тех, кто в 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://stackoverflow.com/a/23212515/72350), в файле нельзя использовать после того, как он был закрыт в Windows. В Linux ответ @Meistro должен работать.

person Diego Jancic    schedule 04.04.2017
comment
На OSX10.12.5 я получил ValueError: I/O operation on closed file - person joe; 20.09.2017
comment
Я не думаю, что вам нужно tmp_file.close() с оператором with. - person LondonAppDev; 01.03.2018