2

Let's say we have a function that converts an image to another format using terminal program convert. Here's the code:

def convert_image(src, dest=None, extension='jpg', quality=100):

    # Create default destination - same path just different extension
    if dest is None:
        dest = src.split('.')[0] + '.' + extension

    try:
        subprocess.check_call(['convert', src, '-quality', quality, dest])
    except CalledProcessError:
        raise ImageException('convert', 'Could not convert image')

    return dest

Now I want to test this function to verify that it works as expected.

The most straight forward way would probably be to just make a unittest that provides the path to a real image and verify that a new image is created with the correct extension. The thing though is that it would be difficult to know if the created image was actually created with the quality set to the correct value and it's kind of awkward to actually convert real images in the test function.

If I call the function like this:

convert_image('/tmp/myimage.png')

What I'm really interested in now is that it from this input sends this array as input to check_call:

['convert', '/tmp/myimage.png', '-quality', 100, '/tmp/myimage.jpg']

If it does then I know that the function is doing the correct thing for this particular case.

So it would be convenient if the function for testing purpose actually just returned this array when I'm testing and only convert images when I'm running the real program.

But it probably isn't to nice to have if-statements in all functions to tell functions to do different things for testing and if I'm never actually using check_call for my testing functions then I could even have a syntax error in that code that's not detected by my test code.

I'm looking for advice / discussion / tips on how to test this code or how to refactor it to make it more testable. I'm not interested in better ways to convert images uses python, this question is purely about code testability.

So, if you were in position to test this function - how would you do it?

user2073343
  • 473
  • 1
  • 5
  • 13
  • can you mock the call to subprocess.check_call? The function should take the values you are passing and return something meaningful if it passes or fails? http://theblobshop.com/pymock/ – Brad Jul 03 '13 at 19:39
  • Yes, I can. This is definitely a reasonable solution. – user2073343 Jul 03 '13 at 20:18

2 Answers2

2

I think you are smart looking to assert the correct array was sent to check_call, making it a proper unit test (and not testing the convert application at the same time). To be able to do this, you are really looking to intercept the call to check_call.

One way to do this is detailed below (note the code is untested but should give an idea of the technique). To do this would be to first encapsulate the call to check_call in a separate function like this (which convert_image now calls instead of subprocess.check_call):

def check_call(args):
    subprocess.check_call(args)

Then, in your test, you can replace check_call with a different function which asserts the arguments that would have been passed to subprocess.check_call. Below shows an example of what the test would look like:

import unittest

class ConvertImageTest(unittest.TestCase):

    def test_convert_image(self):

        # Test version of check_call to do the assertion
        def assert_check_call(args):
            expected = ['convert', '/tmp/myimage.png', '-quality', 100, '/tmp/myimage.jpg']
            self.assertEquals(expected, args)

        # Replace check_call with our test version
        my.module.check_call = assert_check_call

        # Perform the test
        convert_image('/tmp/myimage.png')

Using this approach it is also simple to simulate an exception being thrown and test how you respond to that.

One potential catch - you may have to reverse assigning my.module.check_call afterwards - to be honest not sure if that is required or not (I guess it depends on whether the module gets re-imported between tests, which one would hope is true).

robjohncox
  • 3,639
  • 3
  • 25
  • 51
0

What you want to do here is mock the convert utility. Then you can test without an actual image file. Your test would read what was passed to the mock utility.

In other words, you test the result of the actual "inter-process communication" that's going on here.

Or, as Brad suggested, mock subprocess.check_call, that seems like a better idea.

OregonTrail
  • 8,594
  • 7
  • 43
  • 58