2014-06-13 9 views
33

Ho un'applicazione Django con una vista che accetta un file da caricare. Utilizzando il framework Django REST sto sottoclassi APIView e l'attuazione del metodo post() in questo modo:Come posso testare il caricamento di file binari con il client di prova di django-rest-framework?

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

Ora sto cercando di scrivere un paio di Unittests per garantire l'autenticazione è necessaria e che un file caricato è in realtà trasformati.

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) 

Ma questo non riesce quando il quadro REST tenta di codificare la richiesta

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

Come posso fare il client di prova inviare i dati senza tentare di decodificare come UTF-8?

+1

Pass '{'image': file}' invece – arocks

+0

@arocks Occhi d'aquila! Ho corretto l'errore di battitura nel messaggio, il codice effettivo non ha avuto questo problema. –

+0

Il tuo codice funziona per me. Grazie! – Khoi

risposta

21

Durante il test dei caricamenti di file, è necessario passare l'oggetto flusso nella richiesta, non i dati.

questo è stato sottolineato nei commenti da @arocks

Pass { 'image': file} instead

Ma questo non ha piena spiegare perché era necessario (e anche non corrisponde alla domanda). Per questa specifica domanda, si dovrebbe fare

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) 

Ciò corrisponderà una richiesta di Django standard, dove il file viene passato come un oggetto stream, e Django REST quadro gestisce. Quando passi solo i dati del file, Django e Django REST Framework lo interpretano come una stringa, il che causa problemi perché si aspetta un flusso.

E per chi viene qui in cerca di un altro errore comune, perché il caricamento di file semplicemente non funzionerà, ma i dati del modulo normale saranno: assicurarsi di impostare format="multipart" durante la creazione della richiesta.

dà a questo anche un problema simile, ed è stato sottolineato dalla @RobinElvin nei commenti

It was because I was missing format='multipart'

+11

Per qualche motivo questo mi porta a 400 errori. L'errore restituito è {"file": ["Il file inviato è vuoto."]}. – Divick

+3

Si noti che se non si desidera allocare un tempfile per qualsiasi motivo (perf sarebbe uno), si può anche fare 'tmp_file = BytesIO (b'some text ')'; questo ti darà un flusso binario che può essere passato come un oggetto file. (Https://docs.python.org/3/library/io.html). – Symmetric

+1

Dovevo aggiungere 'tmp_file.seek (0)' prima del 'post', ma per il resto perfetto! Questo mi ha fatto impazzire, quindi grazie! – jaywink

11

Python 3 utenti: assicuratevi di open il file in mode='rb' (leggere, binario). Altrimenti, quando Django chiama read sul file, il codec utf-8 inizierà immediatamente a soffocare. Il file dovrebbe essere decodificato come binario non utf-8, ascii o qualsiasi altra codifica.

# 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

Penso che '{'image': data}' dovrebbe essere '{'image': fp}' nella risposta. Stavo faticando con gli upload fino a quando ho trovato questo post, eppure i miei test non sono passati finché non ho messo il file handle object 'fp' al posto di' data' nel dizionario '{'image::}} descritto sopra. ('{'image': fp}' ha funzionato nel mio caso, '{'image': data}' no.) – DMfll

+0

Aggiornato. Grazie DMfll. – Meistro

0

Per quelli in Windows, la risposta è leggermente diversa. Ho dovuto fare quanto segue:

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) 

La differenza, come ha sottolineato in questa risposta (https://stackoverflow.com/a/23212515/72350), il file non può essere utilizzata solo dopo essere stato chiuso in Windows. Sotto Linux, la risposta di @ Meistro dovrebbe funzionare.

+0

Su OSX10.12.5 Ho ricevuto l'errore "ValueError: I/O su file chiuso" – Sarit

+0

Non penso che sia necessario il comando 'tmp_file.close()' con l'istruzione 'with'. –

Problemi correlati