Search code examples
pythonunit-testingtestingtestability

How would you test this function?


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?


Solution

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