I'm wondering what techniques people use for simplifying the 'size' of code used for unit testing. For example I was trying to marshal an object of the class and testing the marshal'ed object (but this presumes marshal is working correctly).
Consider the class
import unittest
class Nums(object):
def __init__(self, n1_, n2_, n3_):
self.n1, self.n2, self.n3 = n1_, n2_, n3_
def marshal(self):
return "n1 %g, n2 %g, n3 %g"%(self.n1,self.n2,self.n3)
and then the marshaling based, list based, and normal tests
class NumsTests(unittest.TestCase):
def setUp(self):
self.nu = Nums(10,20,30)
def test_init1(self):
self.assertEquals(self.nu.marshal(),"n1 %g, n2 %g, n3 %g"%(10,20,30))
def test_init2(self):
self.assertEquals([self.nu.n1,self.nu.n2,self.nu.n3],[10,21,31])
def test_init3(self):
self.assertEquals(self.nu.n1,10)
self.assertEquals(self.nu.n2,21)
self.assertEquals(self.nu.n3,31)
which give the following errors (since, 20!=21 and 30!=31, my test has a bad initialization or the test conditions are wrong)
AssertionError: 'n1 10, n2 20, n3 30' != 'n1 10, n2 21, n3 31'
AssertionError: [10, 20, 30] != [10, 21, 31]
AssertionError: 20 != 21
The first and second error messages are difficult to understand (since you have to parse the string or list). However, the 3rd technique rapidly explodes in the amount of code used to test complex objects.
Is there a better way to simplify unit tests without creating difficult error messages? And, without depending on the veracity of a marshal function?
[changed test_marshal
to marshal
]
I echo the comments above that you should not have test methods on the actual class you are testing. Functions like test_marshal
should be placed elsewhere (assuming that they do exist for testing and not for general usage), typically in your unit test files. However, setting that aside for the moment, I'd do something like this
import unittest
class Nums(object):
FORMAT = "n1 %g, n2 %g, n3 %g" # make this a variable for easy testing
def __init__(self, n1, n2, n3):
self.n1, self.n2, self.n3 = n1, n2, n3 # no underscores necessary
def test_marshal(self):
return self.FORMAT % (self.n1, self.n2, self.n3)
class NumsTests(unittest.TestCase):
def setUp(self):
self.nums = [10, 20, 30] # make a param list variable to avoid duplication
self.nu = Nums(*self.nums) # Python "apply" syntax
self.nu_nums = [self.nu.n1, self.nu.n2, self.nu.n3] # we'll use this repeatedly
def test_init1(self):
self.assertEquals(self.nu.test_marshal(), self.nu.FORMAT % self.nums )
def test_init2(self):
self.assertEquals(self.nums, self.nu_nums)
def test_init3(self):
for reference, test in zip(self.nums, self.nu_nums):
self.assertEquals(reference, test)
See http://docs.python.org/library/functions.html#apply for an explanation of the apply syntax used above.
By putting the things you're testing into variables, you can avoid duplication of code, which seems to be your primary concern.
As for the error messages being confusing, I guess it depends on how much detail you feel you need. Personally, the fact that my unit tests give me the line of code and values that were expected and not present tends to make it fairly clear what went wrong. However, if you REALLY wanted something that told you specifically which field was incorrect, rather that just the values that didn't match AND you wanted to avoid code duplication, you could write something like this:
class NumsTests(unittest.TestCase):
def setUp(self):
self.nums = {"n1": 10, "n2": 20, "n3": 30} # use a dict, not a list
self.nu = Nums(**self.nums) # same Python "apply" syntax
# test_init1 and test_init2 omitted for space
def test_init3(self):
for attr,val in self.nums.items():
self.assertEqual([attr, val], [attr, getattr(self.nu, val)])
If you ever did have non-matching values, you'd now get errors that look like
AssertionError: ["n1", 10] != ["n1", 11]
and thus you'd know at a glance which field didn't match, instead of having to reason it out based on what the values were. This approach still preserves the code expansion problem, since test_init3 will stay the same size no matter how many parameters you add to your Nums
class.
Note that this use of getattr requires that your __init__
parameters have the same name as the fields in the num class, e.g. they must be named n1
rather than n1_
, etc. An alternative approach would be to use the __dict__ attribute, as described here.
For testing initialization, I recommend not testing via calling the marshal()
function. Not only do you then have to parse out which initializer failed, you also have to figure out whether it's your initialization that's failing or the marshal function itself. The "minimal style" for unit tests I would recommend is to narrow down the focus of what you're testing at any test point as much as is feasible.
If I really had to test the initialization of a whole bunch of fields, I might refactor much the same way as Eli:
class MyTestCase(unittest.TestCase):
def assertFieldsEqual(self, obj, expectedFieldValDict):
for fieldName, fieldVal in expectedFieldValDict.items():
self.assertFieldEqual(obj, fieldName, fieldVal)
def assertFieldEqual(self, obj, fieldName, expectedFieldVal):
actualFieldVal = getattr(obj, fieldName)
if expectedFieldVal != actualFieldVal:
msg = "Field %r doesn't match: expected %r, actual %r" % \
(fieldName, expectedFieldVal, actualFieldVal)
self.fail(msg)
class NumsTests(MyTestCase):
def setUp(self):
self.initFields = {'n1': 10, 'n2': 20, 'n3': 30}
self.nums = Nums(**initFields)
def test_init(self):
self.assertFieldsEqual(self.nums, self.initFields)
"Good grief," I can hear you say, "that's a lot of code!" Well yeah, but the differences are:
assertFieldsEqual
and assertFieldEqual
are reusable functions that have been abstracted to a common test case class which your other test cases can reuse.When it comes time to test your marshal function, you can simply do this:
def test_marshal(self):
expected = '...'
self.assertEqual(expected, self.nums.marshal())
When comparing strings, though, I prefer a method that tells me exactly where the strings diverge. For multiline strings, there's now a method for that in Python 2.7, but I've rolled or cribbed my own methods for this purpose in the past.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With