3

I am currently experimenting with the pytest module to create unit tests for a project I'm working on. I'm trying to test the 'add_point' method which draws an ellipse based on a set of pixels. What I want to do is inspect 'draw' to ensure that the ellipse has been created successfully. Unfortunately I don't know how to go about this, so any help will be appreciated. Here's my code so far:

(A) TheSlicePreviewMaker.py

import os, Image, ImageDraw, ImageFont
from json_importer import json_importer

class SlicePreviewer(object):
    def __init__(self):
        self.screen_size = (470, 470)
        self.background_colour = (86,0,255)
        self.platform_fill_colour = (100, 100, 100)
        self.platform_outline_colour = (0, 0, 0)
        self.platform_window = (0,0,469,469)
        self.point_colour = (0,0,255)
        self.config_object = json_importer("ConfigFile.txt")
        self.image = None

    def initialise_image(self):
        self.image = Image.new('RGB',self.screen_size,self.background_colour)
        draw = ImageDraw.Draw(self.image)
        draw.rectangle(self.platform_window,outline=self.platform_outline_colour,fill=self.platform_fill_colour)
        del draw

    def add_point(self, px, py):
        x1 = px - 1
        y1 = py - 1
        x2 = px + 1
        y2 = py + 1
        draw = ImageDraw.Draw(self.image)
        draw.ellipse((x1,y1,x2,y2),outline=self.point_colour,fill=self.point_colour)
        return draw #del draw

    def save_image(self, file_name):
        self.image.save(file_name, "BMP")

(B) test_TheSlicePreviewMaker.py

from TheSlicePreviewMaker import SlicePreviewer

slice_preview = SlicePreviewer()
class TestSlicePreviewer:
    def test_init(self):
        '''check that the config file object has been created on init'''
        assert slice_preview.config_object != None

    def test_initialise_image(self):
        '''verify if the image has been successfully initialised'''
        assert slice_preview.image.mode == 'RGB'

    def test_add_point(self):
        '''has the point been drawn successfully?'''
        draw = slice_preview.add_point(196,273)
        assert something

import pytest
if __name__ == '__main__':
    pytest.main("--capture=sys -v")

SN: I've run TheSlicePreviewMaker.py separately to check the bitmap file it produces, so I know that the code works. What I want to achieve is unit test this so that each time I don't have to go check the bitmap.

BeeLabeille
  • 174
  • 1
  • 4
  • 16

1 Answers1

7

One approach is to manually inspect the generated image and if looks OK to you, save it next to the test and use a image diffing algorithm (for example ImageChops.difference) to obtain a threshold value that you can use to make sure future test runs are still drawing the same image.

For example:

# contents of conftest.py
from PIL import ImageChops, ImageDraw, Image
import pytest
import os
import py.path
import math
import operator

def rms_diff(im1, im2):
    """Calculate the root-mean-square difference between two images
    Taken from: http://snipplr.com/view/757/compare-two-pil-images-in-python/
    """
    h1 = im1.histogram()
    h2 = im2.histogram()

    def mean_sqr(a,b):
        if not a:
            a = 0.0
        if not b:
            b = 0.0
        return (a-b)**2

    return math.sqrt(reduce(operator.add, map(mean_sqr, h1, h2))/(im1.size[0]*im1.size[1]))


class ImageDiff:
    """Fixture used to make sure code that generates images continues to do so
    by checking the difference of the genereated image against known good versions.
    """

    def __init__(self, request):
        self.directory = py.path.local(request.node.fspath.dirname) / request.node.fspath.purebasename
        self.expected_name = (request.node.name + '.png') 
        self.expected_filename = self.directory / self.expected_name

    def check(self, im, max_threshold=0.0):
        __tracebackhide__ = True
        local = py.path.local(os.getcwd()) / self.expected_name
        if not self.expected_filename.check(file=1):
            msg = '\nExpecting image at %s, but it does not exist.\n'
            msg += '-> Generating here: %s'
            im.save(str(local))
            pytest.fail(msg % (self.expected_filename, local))
        else:
            expected = Image.open(str(self.expected_filename))
            rms_value = rms_diff(im, expected)
            if rms_value > max_threshold:
                im.save(str(local))
                msg = '\nrms_value %s > max_threshold of %s.\n'
                msg += 'Obtained image saved at %s'
                pytest.fail(msg % (rms_value, max_threshold, str(local))) 

@pytest.fixture
def image_diff(request):        
    return ImageDiff(request)

Now you can use the image_diff fixture in your tests. For example:

def create_image():
    """ dummy code that generates an image, simulating some actual code """
    im = Image.new('RGB', (100, 100), (0, 0, 0))
    draw = ImageDraw.Draw(im)
    draw.ellipse((10, 10, 90, 90), outline=(0, 0, 255), 
                 fill=(255, 255, 255))
    return im      


def test_generated_image(image_diff):        
    im = create_image()
    image_diff.check(im) 

The first time your run this test, it will fail with this output:

================================== FAILURES ===================================
____________________________ test_generated_image _____________________________

image_diff = <test_foo.ImageDiff instance at 0x029ED530>

    def test_generated_image(image_diff):
        im = create_image()
>       image_diff.check(im)
E       Failed:
E       Expecting image at X:\temp\sandbox\img-diff\test_foo\test_generated_image.png, but it does not exist.
E       -> Generating here: X:\temp\sandbox\img-diff\test_generated_image.png

You can then manually check the image and if everything is OK, move it to a directory with the same name as the test file, with the name of the test as the file name plus ".png" extension. From now one whenever the test runs, it will check that the image is similar within an acceptable amount.

Suppose you change the code and produce a slightly different image, the test will now fail like this:

================================== FAILURES ===================================
____________________________ test_generated_image _____________________________

image_diff = <test_foo.ImageDiff instance at 0x02A4B788>

    def test_generated_image(image_diff):
        im = create_image()
>       image_diff.check(im)
E       Failed:
E       rms_value 2.52 > max_threshold of 0.0.
E       Obtained image saved at X:\temp\sandbox\img-diff\test_generated_image.png

test_foo.py:63: Failed
========================== 1 failed in 0.03 seconds ===========================

The code needs some polishing but should be a good start. You can find a version of this code here.

Cheers,

Bruno Oliveira
  • 13,694
  • 5
  • 43
  • 41
  • Hi Bruno, thanks for replying. I hadn't thought of this approach before. To be fair it seems a bit long winded but I'll try it out and see how it goes. Cheers – BeeLabeille Mar 27 '15 at 12:54
  • See also [this answer](https://stackoverflow.com/a/56280735/448915) on how to use the built in method `ImageChops.difference` to check two images for equality, without implementing the root-mean-square difference (you don't have a difference threshold though). – Francesco Feltrinelli Dec 27 '20 at 21:53