Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's wrong with this Python mock patch?

I'm having trouble mocking out the an imported module in a unit test. I'm trying to mock the PIL Image class in my module tracker.models using the mock module. I understand you are supposed to mock things where they are used, so I've written @mock.patch('tracker.models.Image') as my decorator for the unit test. I am trying to check whether the downloaded image gets opened as a PIL Image. The mock patch seems to be overwriting the entire Image module. Here is the error I'm getting when I run the test:

File "/home/ubuntu/workspace/tracker/models.py", line 40, in set_photo
    width, height = image.size
ValueError: need more than 0 values to unpack

Here's my unit test:

test_models.py

@responses.activate
@mock.patch('tracker.models.Image')
def test_set_photo(self, mock_pil_image):
    # Initialize data
    hammer = Product.objects.get(name="Hammer")
    fake_url = 'http://www.example.com/prod.jpeg'
    fake_destination = 'Hammer.jpeg'

    # Mock successful image download using sample image. (This works fine)
    with open('tracker/tests/test_data/small_pic.jpeg', 'r') as pic:
        sample_pic_content = pic.read()
    responses.add(responses.GET, fake_url, body=sample_pic_content, status=200, content_type='image/jpeg')

    # Run the actual method
    hammer.set_photo(fake_url, fake_destination)

    # Check that it was opened as a PIL Image
    self.assertTrue(mock_pil_image.open.called,
                    "Failed to open the downloaded file as a PIL image.")

Here is the piece of code it is testing.

tracker/models.py

class Product(models.Model):
    def set_photo(self, url, filename):
        image_request_result = requests.get(url)
        image_request_result.content
        image = Image.open(StringIO(image_request_result.content))

        # Shrink photo if needed
        width, height = image.size  # Unit test fails here
        max_size = [MAX_IMAGE_SIZE, MAX_IMAGE_SIZE]
        if width > MAX_IMAGE_SIZE or height > MAX_IMAGE_SIZE:
            image.thumbnail(max_size)
        image_io = StringIO()
        image.save(image_io, format='JPEG')
        self.photo.save(filename, ContentFile(image_io.getvalue()))
like image 991
Stewart Avatar asked Apr 18 '15 12:04

Stewart


1 Answers

You need to configure the return value of Image.open to include a size attribute:

opened_image = mock_pil_image.open.return_value
opened_image.size = (42, 83)

Now when your function-under-test calls Image.open the returned MagicMock instance will have a size attribute that is a tuple.

You could do the same thing for any other methods or attributes that need to return something.

The opened_image reference is then also useful for testing other aspects of your function-under-test; you can now assert that image.thumbnail and image.save were called:

opened_image = mock_pil_image.open.return_value
opened_image.size = (42, 83)

# Run the actual method
hammer.set_photo(fake_url, fake_destination)

# Check that it was opened as a PIL Image
self.assertTrue(mock_pil_image.open.called,
                "Failed to open the downloaded file as a PIL image.")

self.assertTrue(opened_image.thumbnail.called)
self.assertTrue(opened_image.save.called)

This lets you test very accurately if your thumbnail size logic works correctly, for example, without having to test if PIL is doing what it does; PIL is not being tested here, after all.

like image 173
Martijn Pieters Avatar answered Oct 13 '22 08:10

Martijn Pieters