Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: How to hide Traceback in unit tests for readability?

I find it a bit irritating getting so much details for a simple failed unit test. Is it possible to suppress everything but the actual defined assert message?

Creating test database for alias 'default'...
.F
======================================================================
FAIL: test_get_sales_item_for_company (my_app.tests.SalesItemModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/kave/projects/my/my_app/tests.py", line 61, in test_get_sales_item_for_company
    self.assertEqual(sales_items.count(), 1, 'Expected one sales item for this company, but got %s' % sales_items.count())
AssertionError: Expected one sales item for this company, but got 2

----------------------------------------------------------------------
Ran 2 tests in 0.313s

FAILED (failures=1)
Destroying test database for alias 'default'...

I find this bit unnecessary. I need to know the test name (method) that failed and the assert message. No need for traceback really..

Traceback (most recent call last):
  File "/home/kave/projects/my/my_app/tests.py", line 61, in test_get_sales_item_for_company
    self.assertEqual(sales_items.count(), 1, 'Expected one sales item for this company, but got %s' % sales_items.count())
like image 481
Houman Avatar asked Nov 13 '22 00:11

Houman


1 Answers

Monkey patching to the rescue. You can get rid of the traceback for failures without touching your Django installation by subclassing Django's TestCase as follows:

import types
from django.utils.unittest.result import failfast
from django.test import TestCase

@failfast
def addFailureSansTraceback(self, test, err):
    err_sans_tb = (err[0], err[1], None)
    self.failures.append((test, self._exc_info_to_string(err_sans_tb, test)))
    self._mirrorOutput = True

class NoTraceTestCase(TestCase):
    def run(self, result=None):
        result.addFailure = types.MethodType(addFailureSansTraceback, result)
        super(NoTraceTestCase, self).run(result)

Now just make your test cases subclasses of NoTraceTestCase instead of TestCase and you are good to go. No more tracebacks for failures. (Note exceptions will still print tracebacks. You could monkey-patch those away similarly if you wanted to.)

Here's how it works (with thanks to Jason Pratt for the quick lesson on monkey patching):

  1. Django's test runner calls TestCase's run method for each test run. The result parameter is an instance of the django.utils.unittest.result.TestResult class, which handles showing test results to the user. Whenever a test fails, run makes the following call: result.addFailure(self, sys.exc_info()). That's where the traceback comes from -- as the third item in the tuple returned by sys.exc_info().

  2. Now, simply overriding run with a copy of the original code and tweaking it as needed would work. But the run method is a good 75 lines long, and all that needs to be changed is that one line, and in any case why miss out the chance for some fun with monkey-patching?

  3. The result.addFailure assignment changes the addFailure method in the result object that is passed to NoTraceTestCase's run method to the newly defined addFailureSansTraceback function -- which is first transformed into a result-object compatible method with types.MethodType.

  4. The super call invokes Django's existing TestCase run. Now, when the existing code runs, the call to addFailure will actually call the new version, i.e. addFailureSansTraceback.

  5. addFailureSansTraceback does what the original version of addFailure does -- copying over two lines of code -- except adds a line that replaces the traceback with None (the assignment to err_sans_tb which is used instead of err in the next line). That's it.

  6. Note the original addFailure has a failfast decorator, so that is imported and used. To be honest, I haven't looked at what it does!

Disclaimer: I haven't studied Django's test code thoroughly. This is just a quick patch to get it to work in the common case. Use at your own risk!

like image 78
Ghopper21 Avatar answered Dec 28 '22 06:12

Ghopper21