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


99

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

...
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没有键“ file”的情况,request.FILES['file']有键“。”的情况None

如何设置幸福道路的发帖数据?有人可以告诉我吗?


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

Answers:


109

来自Django文档Client.post

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

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



2
Henning从技术上来说是正确的-这integration test实际上不是一个问题-直到您进入更复杂的代码库为止,甚至与一个真正的测试团队都没有关系
Alvin

在Web框架中,如果要测试视图,则差异不大。通过客户端还是直接从函数中获取响应对于大多数测试而言都是足够相似的。另外,客户可以为您提供更大的灵活性。我个人就是这样用的。
trpt4him

链接更新到相关的Django文档:docs.djangoproject.com/en/dev/topics/testing/tools / ...
冻结

109

我曾经做过同样的事情,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+中,您需要使用bytesobject而不是str。更改"file_content"b"file_content"

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


1
使用您的示例,表单验证为我提供:“上传有效图像。您上传的文件不是图像,也不是损坏的图像。”
antonagestam 2015年

@antonagestam您传递正确的内容类型吗?您的表单是否在验证文件的内容?如果"file_content"需要,则必须是有效的图像标题,以便您的代码认为它是有效的图像。
Danilo Cabello 2015年

JPEG和PNG的适当标题是什么?
antonagestam 2015年

2
这应该被认为是此问题的正确答案。谢谢@DaniloCabello。
mannysz

1
您可以使用base64.b64decode(“ iVBORw0KGgoAAAANSUhEUgAAAAUA” +“ AAAFCAYAAACNbyblAAAAHElEQVQI12P4 // 8 / w38GIAXDIBKE0DHxgljNBAAO” +“ 9TXL0Y4OHwAAAABJRU5ErkJggg =” =“)作为图像内容。
Howdedo

6

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

话虽如此,我发现了您的代码中的几个缺陷。

  • “单元”测试意味着只测试一个功能的 “单元”。因此,如果要测试该视图,则要测试该视图,而文件系统是ergo,不是真正的单元测试。为了使这一点更加清楚。如果运行该测试,并且视图运行良好,但是您没有保存该文件的权限,则测试将因此失败。
  • 另一个重要的是测试速度。如果您正在执行类似TDD的操作,那么测试的执行速度就非常重要。 访问任何I / O都不是一个好主意

因此,我建议您重构视图以使用如下功能:

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

并对此进行一些嘲弄。您可以使用Python Mock

PS:您也可以使用Django Test Client,但这意味着您要添加另一项要测试的东西,因为该客户端利用了Sessions,中间件等。没有什么与单元测试类似。


1
我可能是错的,但似乎他是要进行集成测试,而只是错误地使用了“单元测试”一词。
13年

1
@santiagobasulto我是TDD中的新手,我想加快单元测试的速度。但是我对文件上传有几种看法,这些看法在单元测试期间也将文件上传到远程存储(Amazon S3)。需要时间。您能否扩大答案以详细显示如何避免在测试时访问I / O?
Dmitry Wojciechowski

5
嘿@Dmitry。模拟是去那里的方式。每当您必须访问外部资源时,都应该对其进行模拟。假设您有一个profile_picture在内部使用upload_profile_picture函数的视图。如果要测试该视图,只需模拟内部函数,并确保在测试中调用了该内部函数。这是一个简单的示例:gist.github.com/santiagobasulto/6437356
santiagobasulto

4

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

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

我做了这样的事情:

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。


1

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

来源:KevinEtienne的https://code.djangoproject.com/ticket/23912

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

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)

利用APIRequestFactory的唯一答案
majkelx

0

Django的官方文档所述

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

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的模拟如下:

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

希望这可以帮助。


0

我正在使用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'}
    )
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.