I’ve been testing some image downloading code on Tandem Exchange, trying to make sure that we properly download a profile image for new users when they sign in using one of our social network logins. As I was writing my unit tests, I found myself doing a bit of copy and paste between the class definitions, because I wanted multiple test cases to check the same behaviors with different inputs. Taking this as a sure sign that I was doing something inefficiently, I started looking for ways to parametrize the test cases.
Google pointed me towards one way to do it, though it seemed a bit more work than necessary and involved some fiddling with classes at runtime. Python supports this, of course, but it seemed a bit messy.
The simpler way, which doesn’t offer quite as much flexibility but offers less complexity (and less fiddling with the class at runtime), was to use Python’s mixin facility to compose unit test classes with the instance parameters I wanted.
So let’s say I expect the same conditions to hold true after I download and process any type of image:
- I want the processed image to be stored somewhere on disk.
- I want the processed image to be converted to JPEG format, in truecolor mode, and scaled to 256 x 256 pixels.
- I want to retrieve the processed image from the web address where I’ve published it, and make sure it is identical to the image data I’ve stored on disk (round trip test).
Here’s what that code might look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
class StandardTestsMixin(object):
def setUp(self):
"""
Download a valid test URL.
"""
self.storage_file, self.web_file = download_avatar(self.TEST_URL)
def tearDown(self):
"""
Remove image from disk.
"""
os.remove(self.storage_file)
def test_download_avatar_A(self):
"""
Confirm that the image was downloaded correctly.
"""
self.assertIsNotNone(self.storage_file)
self.assertTrue(os.path.exists(self.storage_file))
def test_download_avatar_B(self):
"""
Confirm that the image is a JPEG and was scaled properly.
"""
I = Image.open(self.storage_file)
self.assertEqual(I.format, 'JPEG')
self.assertEqual(I.mode, 'RGB')
self.assertEqual(I.size, (256, 256))
self.assertIsNone(I.palette)
def test_download_avatar_C(self):
"""
Accessing image via BASE_HREF + web_file works correctly.
"""
TX_AVATAR_URL = BASE_HREF + self.web_file
r = requests.get(TX_AVATAR_URL)
self.assertTrue(r.ok)
# Compare the byte-sizes of what was just retrieved
# with the what is on the local disk.
#
# They must be equal, or something went wrong.
image_local = open(self.storage_file, 'rb').read()
image_gotten = r.content
self.assertEqual(image_local, image_gotten)
class GoodAvatar(StandardTestsMixin, unittest.TestCase):
TEST_URL = 'https://www.tandemexchange.com/static/images/unittest/DSC02879.JPG'
class JpgPaletteAvatar(StandardTestsMixin, unittest.TestCase):
TEST_URL = 'https://www.tandemexchange.com/static/images/unittest/palette.jpg'
class AnimatedGifAvatar(StandardTestsMixin, unittest.TestCase):
TEST_URL = 'https://www.tandemexchange.com/static/images/unittest/animated.gif'
|
So what ends up happening is that the composed classes simply specify which image they want the test functions to run against, and the rest of the test functions run as usual against that input parameter.
One thing readers might notice is the seemingly backwards class inheritance. Turns out (you learn something everyday!) Python thinks about class inheritance declarations from right-to-left, meaning that in the above examples, unittest.TestCase is the root of the inheritance chain. Or another way to look at it is that, for example, GoodAvatar instances will first search in StandardTestsMixin then in unittest.TestCase for inherited methods.
consider using nose-ittr: https://github.com/taykey/nose-ittr
its a simple and clean way to run parametrized tests with nose in python