I have a unit test that I want to show failing as part of a bug report. It's important not only that the test fail with an error, but that the error message very clearly evidences the underlying bug. I do NOT want the test to pass.
Here is a minimal example:
import unittest
import attrs
@attrs.frozen
class FrozenException(Exception):
pass
class BugException(Exception):
pass
class Test(unittest.TestCase):
def testFromNone(self):
raise BugException
def testFromFrozen(self):
try:
raise FrozenException
except FrozenException:
raise BugException
if __name__ == '__main__':
unittest.main()
When I execute this file, I expect (and desire) for both test methods to fail and show BugException
as the reason for the failure. testFromNone
behaves as expected:
$ python3 -m unittest scratch.Test.testFromNone
E
======================================================================
ERROR: testFromNone (scratch.Test.testFromNone)
----------------------------------------------------------------------
Traceback (most recent call last):
...
scratch.BugException
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
However, running testFromFrozen
causes the unit test code itself to crash1, hiding the BugException
deep in the stack trace2:
$ python3 -m unittest scratch.Test.testFromFrozen
Traceback (most recent call last):
...
<very long traceback>
...
attr.exceptions.FrozenInstanceError
It appears that the unittest
framework does some sort of post-processing on exceptions that aren't caught by the test, and that in the course of this processing, it tries to mutate the FrozenException
instance stored in the BugException
's traceback, triggering the FrozenInstanceError
.
How can I work around this so that the test shows the correct error message? I'd prefer to avoid removing the @attrs.frozen
decorator if at all possible3.
Python version 3.11.8, attrs
version 22.2.0.
1This behavior is only observable if the exception is not caught within the body of the test method. If my example is changed to use assertRaises
or an explicit try/catch
, the FrozenInstanceError
is not raised.
2In the real world, beyond the scope of this example, the symptom is even worse because the unexpected error causes the entire test suite to hang, causing no further tests to be executed and the process to just sit there until the test job times out.
3In the real code, the frozen exception class actually has attributes.
You get the expected result in attrs 23.1.0 and later:
EE
======================================================================
ERROR: testFromFrozen (__main__.Test.testFromFrozen)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/hynek/FOSS/attrs/t2.py", line 22, in testFromFrozen
raise FrozenException
FrozenException
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/hynek/FOSS/attrs/t2.py", line 24, in testFromFrozen
raise BugException
BugException
======================================================================
ERROR: testFromNone (__main__.Test.testFromNone)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/hynek/FOSS/attrs/t2.py", line 18, in testFromNone
raise BugException
BugException
----------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (errors=2)
The relevant changes are in https://github.com/python-attrs/attrs/pull/1081 and allow the modification of __traceback__
.