pythonのunittestで自作のassertion methodを追加したときに、不要な行がスタックトレースに紛れ込まなくて良くなる方法について

pythonでassertion methodを追加したい場合はテキトウにメソッドを書いてあげれば良い。Mixinのクラスを定義してあげれば大丈夫。

組み込みのassertion methodの定義

例えば、幾つかのunittest.TestCaseに組み込みのassertion methodの定義を見て真似してあげれば良い。

たとえばunittest/case.pyをのぞくと以下の様なコードがある。

unittest/case.py

class TestCase(object):
...
    def assertTrue(self, expr, msg=None):
        """Check that the expression is true."""
        if not expr:
            msg = self._formatMessage(msg, "%s is not true" % safe_repr(expr))
            raise self.failureException(msg)

testがfailになった場合にfailreExceptionを呼んであげれば良い。

細々とした定義

ちなみに、failreExceptionは以下のような定義になっていて、fail()メソッドを使っている部分もあった。

unittest/case.py

class TestCase(object):
...
    failureException = AssertionError
...
    def fail(self, msg=None):
        """Fail immediately, with the given message."""
        raise self.failureException(msg)

自作のassertion method

以上を踏まえて必ず失敗するassertion methodを作ってみる。

testing.py

class AlwaysFailMixin:
    def assert_always_fail(self):
        self.fail("fail..")

以下のようなテストコードを書く。

00tests.py

import unittest
from testing import AlwaysFailMixin


class Tests(AlwaysFailMixin, unittest.TestCase):
    def test_it(self):
        self.assert_always_fail()


if __name__ == "__main__":
    unittest.main()

実行してみると、以下のようなtracebackが出力される。

$ python 00tests.py
F
======================================================================
FAIL: test_it (__main__.Tests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "00tests.py", line 7, in test_it
    self.assert_always_fail()
  File "$HOME/venvs/my/individual-sandbox/daily/20190616/example_unittest/testing.py", line 3, in assert_always_fail
    self.fail("fail..")
AssertionError: fail..

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

動いてはいるのだけれど、この行が残ってしまうのがちょっと不服。

  File "$HOME/venvs/my/individual-sandbox/daily/20190616/example_unittest/testing.py", line 3, in assert_always_fail
    self.fail("fail..")

これを消す方法が存在するのでひと工夫すると良いという話。

__unittest = True

実は __unittest = True という値をモジュールグローバルに定義してあげると良い感じに動く。

--- testing.py   2019-06-17 03:43:04.748776798 +0900
+++ testing2.py   2019-06-17 03:43:45.248823695 +0900
@@ -1,3 +1,6 @@
+__unittest = True  # <- this!!
+
+
 class AlwaysFailMixin:
     def assert_always_fail(self):
         self.fail("fail..")

実行結果は以下の様になる(すばらしい)。

$ python 01tests.py
F
======================================================================
FAIL: test_it (__main__.Tests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "01tests.py", line 7, in test_it
    self.assert_always_fail()
AssertionError: fail..

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

このあたりドキュメントに書いてあっても良さそうな気がするけれど。書いていない。

どこか情報源になるものは?

少し調べるとstack overflowなどでの情報は見つかる。

あるいはdjangoのtest用のhelperコード的なものにも含まれている。

実際どこで取り除かれているのか?

実際中のコードを覗いてみると以下の部分が対応している。

unittest/result.py

class TestResult(object):
...
    def _is_relevant_tb_level(self, tb):
        return '__unittest' in tb.tb_frame.f_globals

    def _exc_info_to_string(self, err, test):
        """Converts a sys.exc_info()-style tuple of values into a string."""
        exctype, value, tb = err
        # Skip test runner traceback levels
        while tb and self._is_relevant_tb_level(tb):
            tb = tb.tb_next
...

テストも存在しているので信用しても良いものだと思う。

test/test_result.py

class Test_TestResult(unittest.TestCase):
    def testStackFrameTrimming(self):
        class Frame(object):
            class tb_frame(object):
                f_globals = {}
        result = unittest.TestResult()
        self.assertFalse(result._is_relevant_tb_level(Frame))

        Frame.tb_frame.f_globals['__unittest'] = True
        self.assertTrue(result._is_relevant_tb_level(Frame))

おしまい。