Efficiently testing multiple model methods at once in Django
Update 2011-08-03
I have since ditched this idea because “Explicit is better than implicit” and “Readability counts.”
The method listed below is not very readable and very implicit in nature. Basically, if you handed this to someone (or you in 6 months), it wouldn’t be immediately obvious what is doing what. Which is not the reaction you want when tests are failing for some unknown reason.
I’ve settled on separate test methods for each model method/attribute. It’s a little more verbose, but very clear and easy to decipher.
I have left the original post to document my learning process, and to remind myself not to cross that fine line between clever and stupid.
/Update
I recently wrote some tests for a model that had many (eight) methods that calculated various values from the fields of the model. I needed to test the methods under many different conditions and found myself writing eight or so self.assertEqual(...) statements for each test condition. I was repeating myself and I didn’t like it.
To remedy the situation, I made a list of the methods I was calling over and over, and then wrote a simple function to iterate over the listed methods and assert that they returned equal to the values I passed the function. Below is a simplified example of how it worked:
# tests.py
from models import MyModel
class MyModelTest(TestCase):
def test_methods(self):
methods = ('method1', 'method2', 'method3')
def assert_methods(obj, values):
for m, v in zip(methods, values):
ret = getattr(obj, m)()
try:
self.assertEqual(ret, v)
except AssertionError:
raise AssertionError('%s[%s].%s() returned %s expected %s'
% (obj.__class__.__name__, obj.pk, m, ret, v))
# Create an object to test
obj1 = MyModel.objects.create(...)
# Assert that method1(), method2() and method3 return
# 123, 'abc', and None respectively
assert_methods(obj1, (123, 'abc', None))
# Make some change to obj1
obj1.somefield = 18
obj1.save()
# Assert the change on the methods
assert_methods(obj1, (987, 'abc', False))
# Create a different object
obj2 = MyModel.objects.create(...)
# Assert different return values for methods
assert_methods(obj2, (456, 'efg', False))
The vital parts above are the methods list and assert_methods(). methods stores a list of strings naming the methods you want to call on your model instance. assert_methods() takes an object (the model instance) and a list of values to compare to the method’s returns.
The error handling prints out a more helpful message when assertion fails, including the model name, primary key, and method called along with the expected and actual values.
So basically, assert_methods() asserts that methods[0] = values[0], methods[1] = values[1] and methods[n] = values[n], etc.
Of course, this is limited to one type of assertion for all the methods, but it’s really useful when you gotta do the same thing a bunch of times. Anyways, hope this helps someone out there, if so let me know.