Mocking a file for testing an S3 read

Scenario:

from storages.backends.s3boto3 import S3Boto3Storage

class ImageStorage(S3Boto3Storage):
    default_acl = "private"
    bucket_name = settings.IMAGE_BUCKET_NAME
    custom_domain = settings.IMAGE_CUSTOM_DOMAIN
    querystring_auth = True

class Image(models.Model):
    # snip...
    image_file = models.ImageField(storage=ImageStorage())

Since I’m going to be reading from S3 and things will sometimes fail for various reasons, I’d like to be able to test my error handling in the view when I try to create a FileResponse using that image_file.

# views.py

def download_image(request, image_id):
    # snip auth/permissions/etc checks
    image = Image.objects.get(id=image_id)
    try:
        return http.FileResponse(image.image_file, filename=filename, as_attachment=True)
    except IOError:
        logger.exception(
            "Failed to download photo %s from S3", image.id,
        )
        return http.ResponseNotFound('this is a temporary measure')

My question is: what do I need to mock to get the response handling to raise the IOError?

Here’s a rough sample of what I’ve tried:

# tests.py
from unittest.mock import MagicMock, patch
from django.test import TestCase
from .models import Image

class ImageDownloadTestCase(TestCase):

    def test_download_fails_read(self):
        # snip image creation
        mock_image = MagicMock()
        mock_image.__iter__ = MagicMock(side_effect=IOError('foo')
        with patch.object(Image.objects, 'get') as mock:
            mock.return_value = mock_image
            response = self.client.get('/path/to/download/1/')
        self.assertEqual(response.status_code, 404)

I’ve tried both read and __iter__ to no avail. What am I missing?

Just eyeballing it, this looks like what you want: https://stackoverflow.com/questions/45436705/unittest-for-ioerror-exception-handling

PSA: If you are writing your own custom mocks for AWS, moto is a good tool for the job. I have used it for clients to mock dozens of services and it’s great.

Just eyeballing it, this looks like what you want

Right, that’s what I’m trying to do. I’m trying to figure out what foo is in mock.foo.side_effect = IOError() in order for it to be called by Django.

Looking at the source, that’s where I got __iter__ from.

1 Like

Update: it turns out that mocking read is correct. However, because it’s a StreamingResponse, in a test scenario, the exception doesn’t get triggered until I iterate over the response’s streaming_content attribute.

I think I’m out of luck.

Can you mock the FileResponse object to have that class raise the IOError instead? That way the code isn’t mocking the manager and should still trigger the IOError code path that you’re hoping to reach.

Good idea. That makes way too much sense.

Thanks!