Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
174 lines (126 sloc) 5.33 KB

Comparing Django Q Objects

Date: 2016-03-28 12:00
tags:topic:testing, language:python, topic:django
category:Code
summary:A super simple assertion helper for comparing instances of Django's Q objects.
scm_path:content/1603-comparing-django-q-objects.rst

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.

Final testing related notes

  • When a suite has complicated assertions to test regularly, create an assertion helper. Write tests to show that your helper works correctly under various conditions.
  • Tests for assertQEqual are in this gist. (If you spot something missing, please let me know!)
  • Always consider the output of failing tests - the complexity of managing a test suite for a software project can be greatly influenced by how informative assertion errors are when they occur.
  • A secondary assertion helper could be created to check for inequality assertQNotEquals.