Background
Note: A newer version of this post exists with an assertion helper for Python 3 and pytest. Read on for Python 2 and unittest and general background on Q objects…
When programmatically building complex queries in Django ORM, it’s helpful to be able to test the resulting Q object instances against each other.
However, Django’s Q object does not implement __cmp__ and neither does Node which it extends (Node is in the django.utils.tree module).
Unfortunately, that means that comparison of Q objects that are equal fails.
>>> from django.db.models import Q
>>> a = Q(thing='value')
>>> b = Q(thing='value')
>>> assert a == b
Traceback (most recent call last)
...
Assertion Error:
This means that writing unit tests that assert that correct Q objects have been created is hard.
A simple solution
Q objects generate great Unicode representations of themselves:
>>> a = Q(place='Residential') & Q(people__gt=5)
>>> unicode(a)
u"(AND: ('place', 'Residential'), ('people__gt', 5))"
In addition, it is “good” testing practice to write assertion helpers whenever a test suite has complicated assertions to make frequently. This provides an opportunity to DRY out test code and expand on any error messages that are raised on failure.
Therefore a really simple solution is an assertion helper that would compare Q objects by:
- Asserting that left and right sides are both instances of Q.
- Asserting that the Unicode for the left and right sides are identical.
So here’s a mixin containing the assertion helper. It can be added to any class that extends unittest.TestCase (such as Django’s default TestCase):
from django.db.models import Q
class QTestMixin(object):
def assertQEqual(self, left, right):
"""
Assert `Q` objects are equal by ensuring that their
unicode outputs are equal (crappy but good enough)
"""
self.assertIsInstance(left, Q)
self.assertIsInstance(right, Q)
left_u = unicode(left)
right_u = unicode(right)
self.assertEqual(left_u, right_u)
Disadvantage of this method is that it is simplistic and doesn’t find all the Q objects that are identical (see below). However, the advantage is that it provides rich diffs on failure:
class TestFail(TestCase, QTestMixin):
def test_unhappy(self):
"""
Two Q objects are not the same
"""
a = Q(place='Residential')
b = Q(place='Palace')
self.assertQEqual(a, b)
Gives output:
AssertionError: u"(AND: ('place', 'Residential'))" != u"(AND: ('place', 'Palace'))"
- (AND: ('place', 'Residential'))
? ^^^^^^^^^
+ (AND: ('place', 'Palace'))
? ^ +++
Which can be very helpful when trying to track down errors.
See this updated post for a version of this assertion helper for Python 3 with pytest.
The perfect world: Predicate Logic
Since Q objects represent the logic of SQL WHERE clauses they are therefore Python representations of predicates. In an ideal world the predicate logic rules of equality could be used to compare Q objects and this would be built directly into Q.__cmp__.
This would mean that:
# WARNING MAGIC IMAGINARY CODE!
# Commutative would work
>>> a = Q(x=1) | Q(x=2)
>>> b = Q(x=2) | Q(x=1)
>>> a == b
True
# Double negation would work
>>> a = Q(x=1)
>>> b = ~~(Q=1)
>>> a == b
True
# Negation on expression would work
>>> a = ~(Q(x=1) & Q(x=2))
>>> b = ~Q(x=1) | ~Q(x=2)
>>> a == b
True
# END IMAGINATION SECTION
This is probably never going to be implemented in Django, because it would be functionality only used (as far as I can see) for testing. In addition, without a special implementation for rendering Q objects diffs, it would be hard to understand the source of errors when mismatches occur.