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)
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.