Как я могу протестировать загрузку двоичного файла с помощью тестового клиента 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.'})
Теперь я пытаюсь написать пару unittests, чтобы обеспечить аутентификацию и что фактически загруженный файл.
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?
Ответы
Ответ 1
При тестировании загрузки файлов вы должны передавать в запрос объект потока, а не данные.
Это было указано в комментариях @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)
Это будет соответствовать стандартному запросу Django, где файл передается как потоковый объект, и Django REST Framework обрабатывает его. Когда вы просто передаете данные файла, Django и Django REST Framework интерпретируют его как строку, что вызывает проблемы, поскольку он ожидает поток.
А для тех, кто приезжает сюда и ищет другую распространенную ошибку, почему загрузка файлов просто не будет работать, а обычные данные формы: обязательно установите format="multipart"
при создании запроса.
Это также создает аналогичную проблему, о которой @RobinElvin указал в комментариях.
Это потому, что мне не хватало format = 'multipart'
Ответ 2
Пользователи Python 3: убедитесь, что вы open
файл в mode='rb'
(read, binary). В противном случае, когда 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')
Ответ 3
Не так просто понять, как это сделать, если вы хотите использовать метод 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
)
Ответ 4
Для тех, кто в 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)
Разница, указанная в этом ответе (fooobar.com/info/165774/...), не может быть использована после закрытия в Windows. Под Linux ответ @Meistro должен работать.