如何在 django 中对文件上传进行单元测试

Posted

技术标签:

【中文标题】如何在 django 中对文件上传进行单元测试【英文标题】:how to unit test file upload in 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..

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

【问题讨论】:

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

来自 Client.post 上的 Django 文档:

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

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

【讨论】:

相关 Django 文档的链接:docs.djangoproject.com/en/dev/topics/testing/overview/… 死链接,见docs.djangoproject.com/en/1.7/topics/testing/tools/… Henning 在技术上是正确的——这更像是一个integration test——实际上并不重要,直到你进入更复杂的代码库,甚至可能有一个实际的测试团队 在 Web 框架中,如果您正在测试视图,那么差别会小得多。通过客户端获取响应与直接从函数获取响应足够相似,以使大多数测试有效。此外,客户端为您提供了更大的灵活性。这就是我个人使用的。 链接更新到相关的 Django 文档:docs.djangoproject.com/en/dev/topics/testing/tools/…【参考方案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"
        ) 

【讨论】:

以上是关于如何在 django 中对文件上传进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

Django单元测试

如何在 python 中对装饰器工厂输入进行单元测试

如何在骆驼中对石英进行单元测试

如何在订阅Angular7单元测试用例中对代码进行单元测试

如何在 Typescript 中对私有方法进行单元测试

如何在 AngularJS 中对隔离范围指令进行单元测试