2014-06-13 9 views
33

Ich habe eine Django-Anwendung mit einer Ansicht, die eine Datei zum Hochladen akzeptiert. Mit Hilfe der Django REST Framework Ich bin Subklassen APIView und Umsetzung der post() Methode wie folgt:Wie kann ich das Hochladen von Binärdateien mit dem Testclient von django-rest-framework testen?

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

Jetzt ist erforderlich Ich versuche, ein paar Unittests zu schreiben Authentifizierung zu gewährleisten, und dass eine hochgeladene Datei ist eigentlich verarbeitet.

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) 

Aber dies fehlschlägt, wenn das REST-Framework versucht, die Anfrage

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

Wie kann ich den Test-Client die Daten machen zu codieren senden, ohne zu versuchen es als UTF-8 zu entschlüsseln?

+1

Übergeben Sie '{'image': file}' stattdessen – arocks

+0

@arocks Adleraugen! Ich habe den Tippfehler in der Buchung korrigiert, der eigentliche Code hatte dieses Problem nicht. –

+0

Ihr Code funktioniert für mich. Vielen Dank! – Khoi

Antwort

21

Beim Testen von Datei-Uploads, sollten Sie das Stream-Objekt in die Anfrage übergeben, nicht die Daten.

Dies wurde

von @arocks in den Kommentaren darauf hingewiesen

Pass { 'image': file} instead

Aber das ist nicht vollständig erklären, warum es nötig war (und auch die Frage nicht übereinstimmen). Für diese spezielle Frage, sollten Sie

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) 

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

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

tun Dies wird eine Standard Django Anfrage übereinstimmt, in dem die Datei in ein Stream-Objekt übergeben wird, und Django REST-Framework behandelt sie. Wenn Sie die Dateidaten einfach übergeben, interpretieren Django und Django REST Framework sie als String, was Probleme verursacht, da ein Stream erwartet wird.

Und für diejenigen, kommen hier auf einen anderen gemeinsamen Fehler suchen, warum das Hochladen von Dateien wird nicht nur, sondern normale Formulardaten arbeiten: stellen Sie sicher, format="multipart" zu setzen, wenn die Anfrage zu schaffen.

gibt dieser auch ein ähnliches Problem, und wurde von @RobinElvin in den Kommentaren

It was because I was missing format='multipart'

+11

Aus irgendeinem Grund führt dies zu 400 Fehler. Der zurückgegebene Fehler ist {"file": ["Die übermittelte Datei ist leer."]}. – Divick

+3

Beachten Sie, dass Sie, wenn Sie aus irgendeinem Grund keine temporäre Datei zuordnen wollen (perf wäre eins), auch 'tmp_file = BytesIO (b'some text ')'; Dadurch erhalten Sie einen binären Stream, der als Dateiobjekt übergeben werden kann. (https://docs.python.org/3/library/io.html). – Symmetric

+1

Hatte 'tmp_file.seek (0)' vor dem 'post' hinzugefügt, aber ansonsten perfekt! Das hat mich fast verrückt gemacht, danke! – jaywink

11

Python 3 Benutzer darauf hingewiesen: stellen Sie sicher, open die Datei in mode='rb' (Lesen, binär). Andernfalls, wenn Django read in der Datei aufruft, wird der utf-8 Codec sofort anfangen zu ersticken. Die Datei sollte als Binärdatei nicht utf-8, ascii oder eine andere Kodierung dekodiert werden.

# 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

Ich denke, dass '{'image': data}' sollte '{'image': fp}' in der Antwort sein. Ich habe mit Uploads gekämpft, bis ich diesen Post gefunden habe, aber meine Tests sind nicht bestanden, bis ich das Datei-Handle-Objekt 'fp' anstelle von' data' im oben beschriebenen '{'image': data}' -Dictionary eingefügt habe. ('{'image': fp}' funktionierte in meinem Fall, '{'image': data}' nicht. – DMfll

+0

Aktualisiert. Danke DMfll. – Meistro

0

Für diejenigen in Windows ist die Antwort ein bisschen anders. Ich hatte folgendes zu tun:

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) 

Der Unterschied, wie es in dieser Antwort darauf (https://stackoverflow.com/a/23212515/72350), kann die Datei nicht verwendet werden, nachdem es in Windows geschlossen. Unter Linux sollte @ Meistros Antwort funktionieren.

+0

Auf OSX10.12.5 habe ich 'ValueError: I/O-Operation auf geschlossene Datei' – Sarit

+0

Ich glaube nicht, dass Sie die' tmp_file.close() 'mit der Anweisung' with' benötigen. –

Verwandte Themen