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

2021-01-04 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


Working on a Django project? Check out my book Speed Up Your Django Tests which covers loads of best practices so you can write faster, more accurate tests.


Subscribe via RSS, Twitter, or email:

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

Related posts:

Tags: django