como fazer o teste de unidade de upload de arquivo no Django

99

Em meu aplicativo django, eu tenho uma visão que realiza o upload de arquivos. O snippet principal é assim

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

Eu gostaria de fazer o teste de unidade da visualização. Estou planejando testar o caminho feliz, bem como o caminho da falha ... ou seja, o caso em que o request.FILESnão tem 'arquivo' de chave, caso em que request.FILES['file']tem None..

Como faço para configurar os dados de postagem para o caminho feliz? Alguém pode me dizer?

Damon
fonte
conforme você marcou a resposta usando a classe de cliente como correta, provavelmente não está procurando um teste de unidade, mas um teste funcional ...
Henning

Respostas:

109

Da documentação do Django em Client.post:

O envio de arquivos é um caso especial. Para POSTAR um arquivo, você precisa apenas fornecer o nome do campo do arquivo como uma chave e um identificador de arquivo para o arquivo que deseja enviar como um valor. Por exemplo:

c = Client()
with open('wishlist.doc') as fp:
  c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})
Arthur neves
fonte
12
link para o documento relevante do Django: docs.djangoproject.com/en/dev/topics/testing/overview/…
lsh
5
link morto, consulte docs.djangoproject.com/en/1.7/topics/testing/tools/…
Jocelyn delalande
2
Henning está tecnicamente correto - isso seria mais do que um integration test- realmente não importa até que você entre em bases de código mais complexas, talvez até mesmo com uma equipe de teste real
Alvin
Em uma estrutura da web, faz muito menos diferença se você estiver testando visualizações. Obter a resposta por meio do cliente versus diretamente da função é semelhante o suficiente para que a maioria dos testes seja válida. Além disso, o cliente oferece mais flexibilidade. É o que eu uso pessoalmente.
trpt4him
link de atualização para o documento Django relevante: docs.djangoproject.com/en/dev/topics/testing/tools/…
congelado em
109

Eu costumava fazer o mesmo, with open('some_file.txt') as fp:mas depois precisava de imagens, vídeos e outros arquivos reais no repo e também estava testando uma parte de um componente principal do Django que foi bem testado, então, atualmente, é o que tenho feito:

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

Em Python 3.5+ você precisa usar o bytesobjeto em vez de str. Mudar "file_content"parab"file_content"

Está funcionando bem, SimpleUploadedFilecria um InMemoryFileque se comporta como um upload normal e você pode escolher o nome, o conteúdo e o tipo de conteúdo.

Danilo Cabello
fonte
1
Usando seu exemplo, a validação do formulário me dá: "Faça upload de uma imagem válida. O arquivo que você carregou não era uma imagem ou era uma imagem corrompida."
antonagestam
@antonagestam Você está passando o tipo de conteúdo correto? O seu formulário está validando o conteúdo do arquivo? em caso afirmativo, "file_content"precisa ser um cabeçalho de imagem válido para que seu código pense que é uma imagem válida.
Danilo Cabello
Quais são os cabeçalhos apropriados para JPEG e PNG?
antonagestam
2
Esta deve ser considerada a resposta correta para este problema. Obrigado @DaniloCabello.
mannysz
1
Você pode usar o base64.b64decode ("iVBORw0KGgoAAAANSUhEUgAAAAUA" + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4 // 8 / w38GIAXDIBKE0DHxgljNBAAUA" + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4 // 8 / w38GIAXDIBKE0DHxgljNBAAO" + "9TXULg5 anjNBAAO" + "imagem 9TXUL0Yr.
Howdedo
6

Eu recomendo que você dê uma olhada no Django RequestFactory . É a melhor maneira de simular os dados fornecidos na solicitação.

Dito isso, encontrei várias falhas no seu código.

  • teste de "unidade" significa testar apenas um "unidade" de funcionalidade. Portanto, se você quiser testar essa visualização, estará testando a visualização e o sistema de arquivos, ergo, não realmente um teste de unidade. Para deixar este ponto mais claro. Se você executar esse teste e a visualização funcionar bem, mas você não tiver permissões para salvar o arquivo, seu teste falhará por causa disso.
  • Outra coisa importante é a velocidade do teste . Se você estiver fazendo algo como TDD, a velocidade de execução de seus testes é muito importante. Acessar qualquer I / O não é uma boa ideia .

Então, eu recomendo que você refatore sua visão para usar uma função como:

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

E zombe disso. Você pode usar Python Mock .

PS: Você também poderia usar o Django Test Client. Mas isso significaria que você está adicionando outra coisa a mais para testar, porque esse cliente faz uso de Sessões, middlewares, etc. Nada semelhante ao Teste de Unidade.

santiagobasulto
fonte
1
Posso estar errado, mas parece que ele queria fazer um teste de integração e acabou de usar o termo 'teste de unidade' incorretamente.
jooks
1
@santiagobasulto Sou um novato em TDD e gostaria de acelerar meus testes de unidade. Mas tenho vários modos de exibição que lidam com uploads de arquivo que também carregam arquivos para o armazenamento remoto (Amazon S3) durante o teste de unidade. Isso leva tempo. Você poderia expandir sua resposta para mostrar em detalhes como evitar o acesso a E / S durante o teste?
Dmitry Wojciechowski
5
Ei @Dmitry. Mock é o caminho a percorrer. Sempre que você tiver que acessar um recurso externo, você deve zombar dele. Suponha que você tenha uma visão chamada profile_pictureque usa uma upload_profile_picturefunção internamente . Se você quiser testar essa visão, apenas simule a função interna e certifique-se de que ela seja chamada em seu teste. Este é um exemplo simples: gist.github.com/santiagobasulto/6437356
santiagobasulto
4

Eu faço algo assim para meu próprio aplicativo relacionado a eventos, mas você deve ter código mais do que suficiente para continuar com seu próprio 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
fonte
4

Eu fiz algo assim:

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

A função create_image criará a imagem, então você não precisa fornecer o caminho estático da imagem.

Nota: Você pode atualizar o código de acordo com o seu código. Este código para Python 3.6.

Chirag Maliwal
fonte
1

No Django 1.7 há um problema com o TestCase que pode ser resolvido usando open (filepath, 'rb'), mas ao usar o cliente de teste não temos controle sobre ele. Acho que provavelmente é melhor garantir que file.read () retorne sempre bytes.

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

Sem a opção rb, um TypeError é gerado:

TypeError: sequence item 4: expected bytes, bytearray, or an object with the buffer interface, str found
Rômulo Collopy
fonte
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
fonte
A única resposta utilizando APIRequestFactory
majkelx
0

Conforme mencionado na documentação oficial do Django :

O envio de arquivos é um caso especial. Para POSTAR um arquivo, você só precisa fornecer o nome do campo do arquivo como uma chave e um identificador de arquivo para o arquivo que deseja enviar como um valor. Por exemplo:

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

Maiores Informações: Como verificar se o arquivo é passado como argumento para alguma função?

Durante o teste, às vezes queremos ter certeza de que o arquivo é passado como um argumento para alguma função.

por exemplo

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

Nos testes, use o mock do Python algo assim:

# 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
fonte
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 isto ajude.

Tobias Ernst
fonte
0

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

Eu tentei, self.client.postmas encontrei uma Resolver404exceção.

O seguinte funcionou para mim:

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
fonte