Better Exception Output in Django’s Test Runner With better-exceptions

The exceptions - They Live!

Today I learned about the better-exceptions pacakage. It makes exception output better, providing more context and colourization on the terminal.

If you’re using Django’s test framework, you can install better-exceptions during your test runs. It makes it the plain assert statement much more usable.

Plain asserts are clearer to write and read than the various self.assert* functions, so a definite win for tests. pytest’s assert statement rewriting is similar to better-exceptions, and it’s definitely a “killer feature” for pytest users. Whilst I recommend pytest, it can be hard to port existing projects, so using better-exceptions is a nice compromise.

Adding better-exceptions To Django Test Runs

First, you’ll want a custom test runner class. If you don’t already have one, create one as below, in a file like example/test.py. Inside that the test runner’s run_tests() method, you can use a monkey-patch to install better-exceptions into the unittest TestResult class, which is responsible for output of tests. There’s a snippet in the better-exceptions documentation, which I’ve made Python-3-only. Putting it all together:

from unittest.result import TestResult

import better_exceptions
from django.test.runner import DiscoverRunner


class ExampleTestRunner(DiscoverRunner):
    def run_tests(self, *args, **kwargs):
        # Enable better-exceptions for better display of exceptions
        # https://github.com/Qix-/better-exceptions#use-with-unittest
        def exc_info_to_string(self, err, test):
            return "".join(better_exceptions.format_exception(*err))

        TestResult._exc_info_to_string = exc_info_to_string

        super().run_tests(*args, **kwargs)

Second, configure Django to use your test runner by setting TEST_RUNNER:

TEST_RUNNER = "example.test.ExampleTestRunner"

Then when you run tests, you should see nice colourized output. For example I made this broken test:

from django.test import SimpleTestCase


class BrokenTests(SimpleTestCase):
    def test_unequal(self):
        total = 1 + 1
        expected = 3
        assert total == expected

Running it I see this output, with better colourization in my terminal:

$ python manage.py test
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_unequal (example.core.tests.BrokenTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/.../python3.9/unittest/case.py", line 59, in testPartExecutor
    yield
  File "/.../python3.9/unittest/case.py", line 593, in run
    self._callTestMethod(testMethod)
    │                    └ <bound method BrokenTests.test_unequal of <example.core.tests.BrokenTests testMethod=test_unequal>>
    └ <example.core.tests.BrokenTests testMethod=test_unequal>
  File "/.../python3.9/unittest/case.py", line 550, in _callTestMethod
    method()
    └ <bound method BrokenTests.test_unequal of <example.core.tests.BrokenTests testMethod=test_unequal>>
  File "/.../example/core/tests.py", line 8, in test_unequal
    assert total == expected
           │        └ 3
           └ 2
AssertionError: assert total == expected

----------------------------------------------------------------------
Ran 1 test in 0.049s

FAILED (failures=1)

Note the last frame, which shows that total is 2 and expected is 3.

Fin

If you like better-exceptions, also check out its documentation on use in your Django logging configuration.

May your tests be easy to read and write,

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: