wie man eine Unit-Testdatei in Django hochlädt


99

In meiner Django-App habe ich eine Ansicht, die das Hochladen von Dateien ermöglicht. Das Kern-Snippet sieht so aus

...
if  (request.method == 'POST'):
    if request.FILES.has_key('file'):
        file = request.FILES['file']
        with open(settings.destfolder+'/%s' % file.name, 'wb+') as dest:
            for chunk in file.chunks():
                dest.write(chunk)

Ich möchte die Ansicht als Unit-Test testen. Ich plane, sowohl den Happy-Pfad als auch den Fail-Pfad zu testen. Dies ist der Fall, in dem request.FILESder Schlüssel keine 'Datei' request.FILES['file']hat None.

Wie richte ich die Postdaten für den Happy Path ein? Kann mir jemand sagen?


Da Sie die Antwort mit der Client-Klasse als richtig markiert haben, suchen Sie wahrscheinlich nicht nach einem Komponententest, sondern nach einem Funktionstest ...
Henning

Antworten:


109

Ab Django-Dokumenten Client.post:

Das Einreichen von Dateien ist ein Sonderfall. Um eine Datei zu POSTEN, müssen Sie nur den Namen des Dateifelds als Schlüssel und ein Dateihandle für die Datei angeben, die Sie als Wert hochladen möchten. Beispielsweise:

c = Client()
with open('wishlist.doc') as fp:
  c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})

12
Link zum relevanten Django-Dokument: docs.djangoproject.com/de/dev/topics/testing/overview/…
lsh


2
Henning ist technisch korrekt - das wäre eher eine integration test- spielt eigentlich keine Rolle, bis Sie in komplexere Codebasen geraten, vielleicht sogar mit einem tatsächlichen Testteam
Alvin

In einem Webframework macht es viel weniger Unterschied, wenn Sie Ansichten testen. Das Abrufen der Antwort über den Client im Vergleich direkt von der Funktion ist ähnlich genug, damit die meisten Tests gültig sind. Außerdem bietet Ihnen der Client mehr Flexibilität. Das benutze ich persönlich.
trpt4him

Link-Update zum relevanten Django-Dokument: docs.djangoproject.com/en/dev/topics/testing/tools/…
am

109

Früher habe ich das Gleiche getan, with open('some_file.txt') as fp:aber dann brauchte ich Bilder, Videos und andere echte Dateien im Repo. Außerdem habe ich einen Teil einer gut getesteten Django-Kernkomponente getestet. Derzeit habe ich Folgendes getan:

from django.core.files.uploadedfile import SimpleUploadedFile

def test_upload_video(self):
    video = SimpleUploadedFile("file.mp4", "file_content", content_type="video/mp4")
    self.client.post(reverse('app:some_view'), {'video': video})
    # some important assertions ...

In Python 3.5+ müssen Sie bytesstattdessen Objekt verwenden str. Wechseln Sie "file_content"zub"file_content"

Es hat gut funktioniert, SimpleUploadedFileerstellt einen InMemoryFile, der sich wie ein normaler Upload verhält, und Sie können den Namen, den Inhalt und den Inhaltstyp auswählen.


1
Anhand Ihres Beispiels gibt mir die Formularüberprüfung Folgendes: "Laden Sie ein gültiges Bild hoch. Die von Ihnen hochgeladene Datei war entweder kein Bild oder ein beschädigtes Bild."
Antonagestam

@antonagestam Übergeben Sie den richtigen Inhaltstyp? Überprüft Ihr Formular den Inhalt der Datei? Wenn dies "file_content"der Fall ist , muss es sich um einen gültigen Bildheader handeln, damit Ihr Code denkt, dass es sich um ein gültiges Bild handelt.
Danilo Cabello

Was sind die geeigneten Header für JPEG und PNG?
Antonagestam

2
Dies sollte als die richtige Antwort für dieses Problem angesehen werden. Danke @DaniloCabello.
Mannysz

1
Sie können base64.b64decode verwenden ("iVBORw0KGgoAAAANSUhEUgAAAAUA" + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4 // 8 / w38GIAXDIBKE0DHxgljNBAAO" + "9TXL0Y4OHwAAAabJrg).
Howdedo

6

Ich empfehle Ihnen, sich Django RequestFactory anzusehen . Dies ist der beste Weg, um die in der Anfrage angegebenen Daten zu verspotten.

Sagte, ich habe mehrere Fehler in Ihrem Code gefunden.

  • "Einheitentest" bedeutet, nur eine "Einheit" der Funktionalität zu testen . Wenn Sie also diese Ansicht testen möchten, testen Sie die Ansicht und das Dateisystem, also nicht wirklich einen Komponententest. Um diesen Punkt klarer zu machen. Wenn Sie diesen Test ausführen und die Ansicht einwandfrei funktioniert, Sie jedoch keine Berechtigung zum Speichern dieser Datei haben, schlägt Ihr Test aus diesem Grund fehl.
  • Eine andere wichtige Sache ist die Testgeschwindigkeit . Wenn Sie so etwas wie TDD machen, ist die Geschwindigkeit der Ausführung Ihrer Tests wirklich wichtig. Der Zugriff auf E / A ist keine gute Idee .

Daher empfehle ich Ihnen, Ihre Ansicht umzugestalten , um eine Funktion wie die folgende zu verwenden:

def upload_file_to_location(request, location=None): # Can use the default configured

Und verspotten Sie das. Sie können Python Mock verwenden .

PS: Sie könnten auch den Django Test Client verwenden. Dies würde jedoch bedeuten, dass Sie noch weitere Dinge zum Testen hinzufügen, da dieser Client Sitzungen, Middlewares usw. verwendet. Nichts Ähnliches wie Unit Testing.


1
Ich könnte mich irren, aber es scheint, als wollte er einen Integrationstest durchführen und hat den Begriff "Unit Test" nur falsch verwendet.
Jooks

1
@santiagobasulto Ich bin ein Neuling in TDD und möchte meine Unit-Tests beschleunigen. Ich habe jedoch mehrere Ansichten zum Hochladen von Dateien, mit denen Dateien auch während des Komponententests auf den Remotespeicher (Amazon S3) hochgeladen werden. Und das braucht Zeit. Könnten Sie bitte Ihre Antwort erweitern, um detailliert zu zeigen, wie Sie vermeiden können, während des Testens auf E / A zuzugreifen?
Dmitry Wojciechowski

5
Hey @Dmitry. Mock ist der Weg dorthin. Wann immer Sie auf eine externe Ressource zugreifen müssen, sollten Sie sie verspotten. Angenommen, Sie haben eine Ansicht namens profile_picture, die intern eine upload_profile_pictureFunktion verwendet. Wenn Sie diese Ansicht testen möchten, verspotten Sie einfach die interne Funktion und stellen Sie sicher, dass sie bei Ihrem Test aufgerufen wird. Dies ist ein einfaches Beispiel: gist.github.com/santiagobasulto/6437356
santiagobasulto

4

Ich mache so etwas für meine eigene ereignisbezogene Anwendung, aber Sie sollten mehr als genug Code haben, um mit Ihrem eigenen Anwendungsfall fortzufahren

import tempfile, csv, os

class UploadPaperTest(TestCase):

    def generate_file(self):
        try:
            myfile = open('test.csv', 'wb')
            wr = csv.writer(myfile)
            wr.writerow(('Paper ID','Paper Title', 'Authors'))
            wr.writerow(('1','Title1', 'Author1'))
            wr.writerow(('2','Title2', 'Author2'))
            wr.writerow(('3','Title3', 'Author3'))
        finally:
            myfile.close()

        return myfile

    def setUp(self):
        self.user = create_fuser()
        self.profile = ProfileFactory(user=self.user)
        self.event = EventFactory()
        self.client = Client()
        self.module = ModuleFactory()
        self.event_module = EventModule.objects.get_or_create(event=self.event,
                module=self.module)[0]
        add_to_admin(self.event, self.user)

    def test_paper_upload(self):
        response = self.client.login(username=self.user.email, password='foz')
        self.assertTrue(response)

        myfile = self.generate_file()
        file_path = myfile.name
        f = open(file_path, "r")

        url = reverse('registration_upload_papers', args=[self.event.slug])

        # post wrong data type
        post_data = {'uploaded_file': i}
        response = self.client.post(url, post_data)
        self.assertContains(response, 'File type is not supported.')

        post_data['uploaded_file'] = f
        response = self.client.post(url, post_data)

        import_file = SubmissionImportFile.objects.all()[0]
        self.assertEqual(SubmissionImportFile.objects.all().count(), 1)
        #self.assertEqual(import_file.uploaded_file.name, 'files/registration/{0}'.format(file_path))

        os.remove(myfile.name)
        file_path = import_file.uploaded_file.path
        os.remove(file_path)

4

Ich habe so etwas gemacht:

from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.core.files import File
from django.utils.six import BytesIO

from .forms import UploadImageForm

from PIL import Image
from io import StringIO


def create_image(storage, filename, size=(100, 100), image_mode='RGB', image_format='PNG'):
   """
   Generate a test image, returning the filename that it was saved as.

   If ``storage`` is ``None``, the BytesIO containing the image data
   will be passed instead.
   """
   data = BytesIO()
   Image.new(image_mode, size).save(data, image_format)
   data.seek(0)
   if not storage:
       return data
   image_file = ContentFile(data.read())
   return storage.save(filename, image_file)


class UploadImageTests(TestCase):
   def setUp(self):
       super(UploadImageTests, self).setUp()


   def test_valid_form(self):
       '''
       valid post data should redirect
       The expected behavior is to show the image
       '''
       url = reverse('image')
       avatar = create_image(None, 'avatar.png')
       avatar_file = SimpleUploadedFile('front.png', avatar.getvalue())
       data = {'image': avatar_file}
       response = self.client.post(url, data, follow=True)
       image_src = response.context.get('image_src')

       self.assertEquals(response.status_code, 200)
       self.assertTrue(image_src)
       self.assertTemplateUsed('content_upload/result_image.html')

Die Funktion create_image erstellt ein Bild, sodass Sie keinen statischen Bildpfad angeben müssen.

Hinweis: Sie können den Code gemäß Ihrem Code aktualisieren. Dieser Code für Python 3.6.


1

In Django 1.7 gibt es ein Problem mit dem TestCase, das mit open (Dateipfad, 'rb') behoben werden kann. Bei Verwendung des Testclients haben wir jedoch keine Kontrolle darüber. Ich denke, es ist wahrscheinlich am besten sicherzustellen, dass file.read () immer Bytes zurückgibt.

Quelle: https://code.djangoproject.com/ticket/23912 , von KevinEtienne

Ohne die Option rb wird ein TypeError ausgelöst:

TypeError: sequence item 4: expected bytes, bytearray, or an object with the buffer interface, str found

1
from rest_framework.test import force_authenticate
from rest_framework.test import APIRequestFactory

factory = APIRequestFactory()
user = User.objects.get(username='#####')
view = <your_view_name>.as_view()
with open('<file_name>.pdf', 'rb') as fp:
    request=factory.post('<url_path>',{'file_name':fp})
force_authenticate(request, user)
response = view(request)

Die einzige Antwort mit APIRequestFactory
Majkelx

0

Wie in der offiziellen Dokumentation von Django erwähnt :

Das Einreichen von Dateien ist ein Sonderfall. Um eine Datei zu POSTEN, müssen Sie nur den Namen des Dateifelds als Schlüssel und ein Dateihandle für die Datei angeben, die Sie als Wert hochladen möchten. Beispielsweise:

c = Client()
with open('wishlist.doc') as fp:
    c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})

Weitere Informationen: Wie kann ich überprüfen, ob die Datei als Argument an eine Funktion übergeben wird?

Während des Testens möchten wir manchmal sicherstellen, dass die Datei als Argument an eine Funktion übergeben wird.

z.B

...
class AnyView(CreateView):
    ...
    def post(self, request, *args, **kwargs):
        attachment = request.FILES['attachment']
        # pass the file as an argument
        my_function(attachment)
        ...

Verwenden Sie in Tests Pythons Mock ungefähr so:

# Mock 'my_function' and then check the following:

response = do_a_post_request()

self.assertEqual(mock_my_function.call_count, 1)
self.assertEqual(
    mock_my_function.call_args,
    call(response.wsgi_request.FILES['attachment']),
)

0
from django.test import Client
from requests import Response

client = Client()
with open(template_path, 'rb') as f:
    file = SimpleUploadedFile('Name of the django file', f.read())
    response: Response = client.post(url, format='multipart', data={'file': file})

Hoffe das hilft.


0

Ich benutze Python == 3.8.2, Django == 3.0.4, djangorestframework == 3.11.0

Ich habe es versucht, self.client.postaber eine bekommenResolver404 Ausnahme bekommen.

Folgendes hat bei mir funktioniert:

import requests
upload_url='www.some.com/oaisjdoasjd' # your url to upload
with open('/home/xyz/video1.webm', 'rb') as video_file:
    # if it was a text file we would perhaps do
    # file = video_file.read()
    response_upload = requests.put(
        upload_url,
        data=video_file,
        headers={'content-type': 'video/webm'}
    )
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.