cómo cargar un archivo de prueba unitaria en django

99

En mi aplicación django, tengo una vista que logra la carga de archivos. El fragmento de código central es así

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

Me gustaría probar la vista unitariamente. Estoy planeando probar la ruta feliz así como la ruta fallida ... es decir, el caso donde request.FILESno tiene un 'archivo' clave, caso donde request.FILES['file']tiene None...

¿Cómo configuro los datos de la publicación para el camino feliz? ¿Alguien puede decírmelo?

demonio
fuente
como marcó la respuesta usando la clase de cliente como correcta, probablemente no esté buscando una prueba unitaria, sino una prueba funcional ...
Henning

Respuestas:

109

De los documentos de Django en Client.post:

El envío de archivos es un caso especial. Para PUBLICAR un archivo, solo necesita proporcionar el nombre del campo del archivo como clave y un identificador de archivo para el archivo que desea cargar como valor. Por ejemplo:

c = Client()
with open('wishlist.doc') as fp:
  c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})
Arthur Neves
fuente
12
enlace al documento de Django relevante: docs.djangoproject.com/en/dev/topics/testing/overview/…
lsh
5
enlace muerto, consulte docs.djangoproject.com/en/1.7/topics/testing/tools/…
Jocelyn delalande
2
Henning es técnicamente correcto, esto sería más integration testbien, en realidad no importa hasta que ingrese a bases de código más complejas, tal vez incluso con un equipo de prueba real
Alvin
En un marco web, la diferencia es mucho menor si está probando vistas. Obtener la respuesta a través del cliente frente a directamente desde la función es lo suficientemente similar para que la mayoría de las pruebas sean válidas. Además, el cliente te da más flexibilidad. Eso es lo que yo uso, personalmente.
trpt4him
actualización del enlace al documento de Django relevante: docs.djangoproject.com/en/dev/topics/testing/tools/…
congelado el
109

Solía ​​hacer lo mismo, with open('some_file.txt') as fp:pero luego necesitaba imágenes, videos y otros archivos reales en el repositorio y también estaba probando una parte de un componente central de Django que está bien probado, así que actualmente esto es lo que he estado haciendo:

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 ...

En Python 3.5+ necesitas usar bytesobject en lugar de str. Cambiar "file_content"ab"file_content"

Ha funcionado bien, SimpleUploadedFilecrea un InMemoryFilearchivo que se comporta como una carga normal y puede elegir el nombre, el contenido y el tipo de contenido.

Danilo Cabello
fuente
1
Usando su ejemplo, la validación del formulario me da: "Cargue una imagen válida. El archivo que cargó no era una imagen o una imagen dañada".
antonagestam
@antonagestam ¿Está pasando el tipo de contenido correcto? ¿Su formulario está validando el contenido del archivo? si es así, "file_content"debe ser un encabezado de imagen válido para que su código crea que es una imagen válida.
Danilo Cabello
¿Cuáles son los encabezados adecuados para JPEG y PNG?
antonagestam
2
Esta debe considerarse la respuesta correcta a este problema. Gracias @DaniloCabello.
mannysz
1
Puede usar base64.b64decode ("iVBORw0KGgoAAAANSUhEUgAAAAUA" + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4 // 8 / w38GIAXDIBKE0DHxgljNBAAO" + "9TXL0YABGGGUAAO", crea una imagen real como una imagen.
Howdedo
6

Te recomiendo que eches un vistazo a Django RequestFactory . Es la mejor manera de simular los datos proporcionados en la solicitud.

Dicho esto, encontré varios defectos en tu código.

  • Prueba de "unidad" significa probar solo una "unidad" de funcionalidad. Entonces, si desea probar esa vista, estaría probando la vista y el sistema de archivos, ergo, no realmente una prueba unitaria. Para aclarar este punto. Si ejecuta esa prueba y la vista funciona bien, pero no tiene permisos para guardar ese archivo, su prueba fallará debido a eso.
  • Otra cosa importante es la velocidad de prueba . Si está haciendo algo como TDD, la velocidad de ejecución de sus pruebas es realmente importante. Acceder a cualquier E / S no es una buena idea .

Por lo tanto, te recomiendo que refactorices tu vista para usar una función como:

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

Y burlarte de eso. Puede utilizar Python Mock .

PD: También podrías usar Django Test Client Pero eso significaría que estás agregando otra cosa más para probar, porque ese cliente hace uso de Sessions, middlewares, etc. Nada similar a Unit Testing.

santiagobasulto
fuente
1
Podría estar equivocado, pero parece que él pretendía hacer una prueba de integración y simplemente usó el término 'prueba unitaria' incorrectamente.
bromas
1
@santiagobasulto Soy un novato en TDD y me gustaría acelerar mis pruebas unitarias. Pero también tengo varias vistas relacionadas con la carga de archivos que cargan archivos en el almacenamiento remoto (Amazon S3) durante las pruebas unitarias. Y eso lleva tiempo. ¿Podría ampliar su respuesta para mostrar en detalle cómo evitar el acceso a E / S durante la prueba?
Dmitry Wojciechowski
5
Hola @Dmitry. Mock es el camino a seguir. Siempre que tenga que acceder a un recurso externo, debe burlarse de él. Suponga que tiene una vista llamada profile_pictureque usa internamente una upload_profile_picturefunción. Si desea probar esa vista, simplemente simule la función interna y asegúrese de que se llame en su prueba. Este es un ejemplo simple: gist.github.com/santiagobasulto/6437356
santiagobasulto
4

Hago algo como esto para mi propia aplicación relacionada con eventos, pero debería tener código más que suficiente para continuar con su propio caso de uso

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)
super9
fuente
4

Hice algo como eso:

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

La función create_image creará una imagen, por lo que no es necesario que proporcione una ruta estática de la imagen.

Nota: Puede actualizar el código según su código. Este código para Python 3.6.

Chirag Maliwal
fuente
1

En Django 1.7 hay un problema con TestCase que se puede resolver usando open (filepath, 'rb') pero cuando usamos el cliente de prueba no tenemos control sobre él. Creo que probablemente sea mejor asegurarse de que file.read () devuelva siempre bytes.

fuente: https://code.djangoproject.com/ticket/23912 , por KevinEtienne

Sin la opción rb, se genera un TypeError:

TypeError: sequence item 4: expected bytes, bytearray, or an object with the buffer interface, str found
Rômulo Collopy
fuente
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)
Suvodeep Dubey
fuente
La única respuesta que utiliza APIRequestFactory
majkelx
0

Como se menciona en la documentación oficial de Django :

El envío de archivos es un caso especial. Para PUBLICAR un archivo, solo necesita proporcionar el nombre del campo del archivo como clave y un identificador de archivo para el archivo que desea cargar como valor. Por ejemplo:

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

Más información: ¿Cómo comprobar si el archivo se pasa como argumento a alguna función?

Durante las pruebas, a veces queremos asegurarnos de que el archivo se pase como argumento a alguna función.

p.ej

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

En las pruebas, use el simulacro de Python algo como esto:

# 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']),
)
Dipen Dadhaniya
fuente
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})

Espero que esto ayude.

Tobias Ernst
fuente
0

Estoy usando Python == 3.8.2, Django == 3.0.4, djangorestframework == 3.11.0

Lo intenté self.client.postpero obtuve una Resolver404excepción.

Lo siguiente funcionó para mí:

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'}
    )
Aseem
fuente