138

In my django app, I have a view which accomplishes file upload.The core snippet is like this

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

I would like to unit test the view.I am planning to test the happy path as well as the fail path..ie,the case where the request.FILES has no key 'file' , case where request.FILES['file'] has None..

How do I set up the post data for the happy path?Can somebody tell me?

damon
  • 8,127
  • 17
  • 69
  • 114
  • as you marked the answer using client class as right, you probably aren't looking for a unit test, but a functional test... – Henning Feb 12 '16 at 15:08
  • https://docs.djangoproject.com/en/dev/topics/testing/tools/#django.test.Client.post then `with open('wishlist.doc', 'rb') as fp: ...` – guettli Nov 22 '22 at 10:40

15 Answers15

164

I used to do the same with open('some_file.txt') as fp: but then I needed images, videos and other real files in the repo and also I was testing a part of a Django core component that is well tested, so currently this is what I have been doing:

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

In Python 3.5+ you need to use bytes object instead of str. Change "file_content" to b"file_content"

It's been working fine, SimpleUploadedFile creates an InMemoryFile that behaves like a regular upload and you can pick the name, content and content type.

Community
  • 1
  • 1
Danilo Cabello
  • 2,814
  • 1
  • 23
  • 26
144

From Django docs on Client.post:

Submitting files is a special case. To POST a file, you need only provide the file field name as a key, and a file handle to the file you wish to upload as a value. For example:

c = Client()
with open('wishlist.doc') as fp:
  c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})
DocZerø
  • 8,037
  • 11
  • 38
  • 66
Arthur Neves
  • 11,840
  • 8
  • 60
  • 73
  • 12
    link to the relevant Django doc: https://docs.djangoproject.com/en/dev/topics/testing/overview/#django.test.client.Client.post – lsh Aug 18 '13 at 19:44
  • 5
    dead link, see https://docs.djangoproject.com/en/1.7/topics/testing/tools/#django.test.Client.post – Jocelyn delalande Sep 08 '14 at 16:03
  • 2
    Henning is technically correct -- this would be more of an `integration test` -- doesn't actually matter until you get into more complex code bases maybe even with an actual test team – Alvin May 02 '17 at 18:49
  • In a web framework, it makes much less difference if you're testing views. Getting the response via the client vs. directly from the function is similar enough for most tests to be valid. Plus the client gives you more flexibility. That's what I use, personally. – trpt4him Mar 15 '19 at 19:27
  • 4
    link update to the relevant Django doc: https://docs.djangoproject.com/en/dev/topics/testing/tools/#django.test.Client.post – freezed Jun 14 '19 at 09:42
  • Should add 'rb' mode – mrblue May 07 '21 at 08:07
7

I recommend you to take a look at Django RequestFactory. It's the best way to mock data provided in the request.

Said that, I found several flaws in your code.

  • "unit" testing means to test just one "unit" of functionality. So, if you want to test that view you'd be testing the view, and the file system, ergo, not really unit test. To make this point more clear. If you run that test, and the view works fine, but you don't have permissions to save that file, your test would fail because of that.
  • Other important thing is test speed. If you're doing something like TDD the speed of execution of your tests is really important. Accessing any I/O is not a good idea.

So, I recommend you to refactor your view to use a function like:

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

And do some mocking on that. You can use Python Mock.

PS: You could also use Django Test Client But that would mean that you're adding another thing more to test, because that client make use of Sessions, middlewares, etc. Nothing similar to Unit Testing.

freezed
  • 1,269
  • 1
  • 17
  • 34
santiagobasulto
  • 11,320
  • 11
  • 64
  • 88
  • 1
    I could be wrong, but it seems like he meant to integration test and just used the term 'unit test' incorrectly. – jooks Apr 26 '13 at 13:52
  • 1
    @santiagobasulto I'm a newbie in TDD and I'd like to speed up my unit testing. But I have several views dealing with file uploads which upload files to the remote storage (Amazon S3) during unit testing as well. An that takes time. Could you please expand your answer to show in details how to avoid accessing I/O while testing? – Dmitry Wojciechowski Sep 04 '13 at 04:47
  • 7
    Hey @Dmitry. Mock is the way to go there. Whenever you have to access an external resource you should mock it. Suppose you have a view called `profile_picture` that internally use a `upload_profile_picture` function. If you want to test that view, just mock the internal function and make sure it's called on your test. This is a simple example: https://gist.github.com/santiagobasulto/6437356 – santiagobasulto Sep 04 '13 at 14:02
6

I do something like this for my own event related application but you should have more than enough code to get on with your own use case

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
  • 29,181
  • 39
  • 119
  • 172
5

I did something like that :

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 function will create image so you don't need to give static path of image.

Note : You can update code as per you code. This code for Python 3.6.

Chirag Maliwal
  • 442
  • 11
  • 25
3
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
  • 109
  • 1
  • 6
  • The only answer utilizing APIRequestFactory – majkelx Feb 13 '20 at 18:35
  • This is the perfect answer for `unittest`. The rest of the answers are all making `client.post` calls which makes them suitable for integration tests. The only thing this answer is missing is use of `SimpleUploadedFile` instead of `fp` object. – Sarang Apr 21 '22 at 16:42
2

As mentioned in Django's official documentation:

Submitting files is a special case. To POST a file, you need only provide the file field name as a key, and a file handle to the file you wish to upload as a value. For example:

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

More Information: How to check if the file is passed as an argument to some function?

While testing, sometimes we want to make sure that the file is passed as an argument to some function.

e.g.

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

In tests, use Python's mock something like this:

# 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
  • 4,550
  • 2
  • 16
  • 24
2

if you want to add other data with file upload then follow the below method

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

The only file_name_to_receive_on_backend will be received as a file other params received normally as post paramas.

Zubair Hassan
  • 776
  • 6
  • 14
1

In Django 1.7 there's an issue with the TestCase wich can be resolved by using open(filepath, 'rb') but when using the test client we have no control over it. I think it's probably best to ensure file.read() returns always bytes.

source: https://code.djangoproject.com/ticket/23912, by KevinEtienne

Without rb option, a TypeError is raised:

TypeError: sequence item 4: expected bytes, bytearray, or an object with the buffer interface, str found
Rômulo Collopy
  • 944
  • 9
  • 13
1
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})

Hope this helps.

Tobias Ernst
  • 4,214
  • 1
  • 32
  • 30
1

I am using django rest framework and I had to test the upload of multiple files.

I finally get it by using format="multipart" in my APIClient.post request.

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")
1

Very handy solution with 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"
        ) 
mka
  • 153
  • 5
0

I am using Python==3.8.2 , Django==3.0.4, djangorestframework==3.11.0

I tried self.client.post but got a Resolver404 exception.

Following worked for me:

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
  • 5,848
  • 7
  • 45
  • 69
0

I am using GraphQL, upload for test:

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

code in class mutation

@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
    )
big-vl
  • 134
  • 1
  • 7
0

Adapting @danilo-cabello example for uploading multiple files -

from django.core.files.uploadedfile import SimpleUploadedFile

def test_upload_video(self):
    videos = [
        SimpleUploadedFile(name, "file_content", content_type="video/mp4")
        for name in ("file_1.mp4", "file_2.mp4")
    ]
    # `video` or whatever the name of the `FileField` is ...
    self.client.post(reverse('app:some_view'), data={'video': videos})

I didn't see a similar example, & was thrown off by using files instead of data on self.client which resulted in an empty request.FILES in my view code on test runs

rdmolony
  • 601
  • 1
  • 7
  • 15