James CookeJames Cooke

Comparing Django Q Objects in Python 3 with pytest

Background

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.

    Args:
        left (Q)
        right (Q)

    Raises:
        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.

  • AAA Part 2: Extracting Arrange code to make fixtures

    This post explores how to extract arrangement code when working with the Arrange Act Assert pattern so that it can be used with certainty across the test suite.