James CookeJames Cooke

Comparing Django Q Objects in Python 3 with pytest


In a previous post I wrote about comparing Django’s Q object instances. The original code was Python 2 with unittest and was due for an update.

The previous issue with comparing Django’s Q objects remains the same:

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.

A simple Python 3 solution

The following is a Python 3.6 assertion helper for use with pytest that uses the original strategy of comparing the string versions of the Q objects.

from django.db.models import Q

def assert_q_equal(left, right):
    Test two Q objects for equality. Does is not match commutative.

        left (Q)
        right (Q)

        AssertionError: When -
            * `left` or `right` are not an instance of `Q`
            * `left` and `right` are not considered equal.
    assert isinstance(left, Q), f'{left.__class__} is not subclass of Q'
    assert isinstance(right, Q), f'{right.__class__} is not subclass of Q'
    assert str(left) == str(right), f'Q{left} != Q{right}'

This time the helper is just a function rather than a mixin for unittest.TestCase.

isinstance is used for comparison so that any instance of a class derived from Q can also be matched. The assertions have secondary expressions in the form of f-strings to give helpful output without raising a custom assertion.

When two Q instances do not match, pytest shows the following output:

______________________ test_neq_multi_not_commutative ______________________
test_assert_q_equal.py:83: in test_neq_multi_not_commutative
    assert_q_equal(q_a, q_b)
test_assert_q_equal.py:22: in assert_q_equal
    assert str(left) == str(right), f'Q{left} != Q{right}'
E   AssertionError: Q(AND: ('speed', 12), ('direction', 'north')) != Q(AND: ('direction', 'north'), ('speed', 12))
E   assert "(AND: ('spee...n', 'north'))" == "(AND: ('direc...'speed', 12))"
E     - (AND: ('speed', 12), ('direction', 'north'))
E     + (AND: ('direction', 'north'), ('speed', 12))
==================== 1 failed, 7 passed in 0.07 seconds ====================

The important thing is to adjust your assertion helpers to best fit the needs of your test suite and team.

  • Django Factory Audit

    Exploring the various model instance factories available for Django. Each factory is tested based on the quality of the instances it creates. Bonus: talk version and slides also available.

  • Comparing Django Q Objects

    A super simple assertion helper for comparing instances of Django’s Q objects.

  • Cleaner unit testing with the Arrange Act Assert pattern

    My PyConUK 2016 talk about the AAA pattern for unit tests and how using it can help us all make our tests cleaner, easier to read and as Pythonic as possible.