【问题标题】:how to unit test file upload in django如何在 django 中对文件上传进行单元测试
【发布时间】:2012-06-25 14:12:55
【问题描述】:

在我的django应用程序中,我有一个完成文件上传的视图。核心sn-p是这样的

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

我想对视图进行单元测试。我计划测试快乐路径和失败路径。即,request.FILES 没有密钥“文件”的情况,request.FILES['file'] 有的情况None..

如何设置快乐路径的帖子数据?有人可以告诉我吗?

【问题讨论】:

  • 当您使用客户端类将答案标记为正确时,您可能不是在寻找单元测试,而是功能测试......

标签: django unit-testing file-upload


【解决方案1】:

来自 Client.post 上的 Django 文档:

提交文件是一种特殊情况。要发布文件,您只需要 提供文件字段名称作为键,以及文件的文件句柄 您希望作为值上传。例如:

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

【讨论】:

【解决方案2】:

我建议你看看 Django RequestFactory。这是模拟请求中提供的数据的最佳方式。

话说,我在你的代码中发现了几个缺陷。

  • “单元”测试意味着测试一个“单元”功能。所以, 如果您想测试该视图,您将测试该视图和文件 系统,因此,不是真正的单元测试。为了更清楚地说明这一点。如果 您运行该测试,视图工作正常,但您没有 保存该文件的权限,您的测试将因此失败。
  • 其他重要的是测试速度。如果你正在做类似的事情 TDD 测试的执行速度非常重要。 访问任何 I/O 都不是一个好主意。

因此,我建议您重构您的视图以使用如下函数:

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

并对此进行一些嘲笑。你可以使用Python Mock。

PS:你也可以使用 Django Test Client 但这意味着你要添加另一个东西来测试,因为客户端使用会话、中间件等。没有什么类似于单元测试。

【讨论】:

  • 我可能是错的,但似乎他的意思是集成测试,只是错误地使用了“单元测试”这个词。
  • @santiagobasulto 我是 TDD 的新手,我想加快我的单元测试。但是我有几个处理文件上传的视图,它们在单元测试期间也将文件上传到远程存储(Amazon S3)。一个需要时间的。您能否扩展您的答案以详细说明如何避免在测试时访问 I/O?
  • 嘿@Dmitry。模拟是去那里的方式。每当您必须访问外部资源时,您都应该模拟它。假设您有一个名为profile_picture 的视图,它在内部使用upload_profile_picture 函数。如果您想测试该视图,只需模拟内部函数并确保在您的测试中调用它。这是一个简单的例子:gist.github.com/santiagobasulto/6437356
【解决方案3】:

我为自己的事件相关应用程序做了类似的事情,但你应该有足够的代码来处理你自己的用例

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】:

    我曾经做过同样的with open('some_file.txt') as fp:,但后来我需要存储库中的图像、视频和其他真实文件,而且我正在测试一个经过良好测试的 Django 核心组件的一部分,所以目前这就是我所拥有的一直在做:

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

    在 Python 3.5+ 中,您需要使用 bytes 对象而不是 str。将"file_content" 更改为b"file_content"

    运行良好,SimpleUploadedFile 创建了一个 InMemoryFile,其行为类似于常规上传,您可以选择名称、内容和内容类型。

    【讨论】:

    • 使用你的例子,表单验证给了我:“上传一个有效的图像。你上传的文件不是图像或损坏的图像。”
    • @antonagestam 您是否传递了正确的内容类型?您的表单是否在验证文件的内容?如果是这样,"file_content" 需要是有效的图像标题,这样您的代码才会认为它是有效的图像。
    • JPEG 和 PNG 的适当标头是什么?
    • 这应该被认为是这个问题的正确答案。谢谢@DaniloCabello。
    • 您可以使用 base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAUA" + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO" + "9TXL0Y4OHwAAAABJRU5ErkJggg==") 作为图像内容,这样可以创建真实的图像。
    【解决方案5】:

    在 Django 1.7 中,TestCase 存在一个问题,可以通过使用 open(filepath, 'rb') 来解决,但是在使用测试客户端时,我们无法控制它。我认为最好确保 file.read() 总是返回字节。

    来源:https://code.djangoproject.com/ticket/23912,作者:KevinEtienne

    如果没有 rb 选项,则会引发 TypeError:

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

    【讨论】:

      【解决方案6】:

      我做了类似的事情:

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

      create_image 函数将创建图像,因此您无需提供图像的静态路径。

      注意:您可以根据自己的代码更新代码。 此代码适用于 Python 3.6。

      【讨论】:

        【解决方案7】:
        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)
        

        【讨论】:

        • 使用 APIRequestFactory 的唯一答案
        【解决方案8】:

        如Django's official documentation中所述:

        提交文件是一种特殊情况。要发布文件,您只需提供文件字段名称作为键,并提供要上传的文件的文件句柄作为值。例如:

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

        更多信息:如何检查文件是否作为参数传递给某个函数?

        在测试时,有时我们希望确保文件作为参数传递给某个函数。

        例如

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

        在测试中,使用Python's mock 类似这样的东西:

        # 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']),
        )
        

        【讨论】:

          【解决方案9】:
          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})
          

          希望这会有所帮助。

          【讨论】:

            【解决方案10】:

            我正在使用 Python==3.8.2、Django==3.0.4、djangorestframework==3.11.0

            我尝试了self.client.post,但得到了Resolver404 异常。

            以下对我有用:

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

            【讨论】:

              【解决方案11】:

              如果您想通过文件上传添加其他数据,请按照以下方法

              file = open('path/to/file.txt', 'r', encoding='utf-8')

                  data = {
                      'file_name_to_receive_on_backend': file,
                      'param1': 1,
                      'param2': 2,
                      .
                      .
                  }
              
                  response = self.client.post("/url/to/view", data, format='multipart')`
              

              唯一的file_name_to_receive_on_backend 将作为文件接收,其他参数通常作为后参数接收。

              【讨论】:

                【解决方案12】:

                我正在使用 django rest 框架,我必须测试多个文件的上传。

                我终于在我的APIClient.post 请求中使用了format="multipart"。

                from rest_framework.test import APIClient
                ...
                    self.client = APIClient()
                    with open('./photo.jpg', 'rb') as fp:
                        resp = self.client.post('/upload/',
                                                {'images': [fp]},
                                                format="multipart")
                

                【讨论】:

                  【解决方案13】:

                  我正在使用 GraphQL,上传测试:

                  with open('test.jpg', 'rb') as fp:
                      response = self.client.execute(query, variables, data={'image': [fp]})
                  

                  类突变中的代码

                  @classmethod
                  def mutate(cls, root, info, **kwargs):
                      if image := info.context.FILES.get("image", None):
                          kwargs["image"] = image
                      TestingMainModel.objects.get_or_create(
                          id=kwargs["id"], 
                          defaults=kwargs
                      )
                  

                  【讨论】:

                    【解决方案14】:

                    mock 非常方便的解决方案

                    from django.test import TestCase, override_settings
                    #use your own client request factory
                    from my_framework.test import APIClient
                    
                    from django.core.files import File
                    import tempfile
                    from pathlib import Path
                    import mock
                    
                    image_mock = mock.MagicMock(spec=File)
                    image_mock.name = 'image.png' # or smt else
                    
                    class MyTest(TestCase):
                    
                        # I assume we want to put this file in storage
                        # so to avoid putting garbage in our MEDIA_ROOT 
                        # we're using temporary storage for test purposes
                        @override_settings(MEDIA_ROOT=Path(tempfile.gettempdir()))
                        def test_send_file(self):
                            client = APIClient()
                            client.post(
                                '/endpoint/'
                                {'file':image_mock},
                                format="multipart"
                            ) 
                    

                    【讨论】:

                      猜你喜欢
                      • 2014-12-07
                      • 1970-01-01
                      • 1970-01-01
                      • 2013-09-29
                      • 1970-01-01
                      • 2014-02-23
                      • 2021-09-18
                      相关资源
                      最近更新 更多