2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

unittest moduleを使ったテストでの長い文字列同士の比較のエラーメッセージを変更する

Last updated at Posted at 2016-12-24

はじめに

pythonの標準ライブラリのunittestで長い文字列同時の比較を行なった時のエラーメッセージが分かりづらいと感じるときがある。このエラーメッセージの出力を変更する方法を調べた。

方法は2つあり好きな方を選べば良い。

  • 特別な assertion メソッドを作る
  • assertEqual() で呼び出されるメソッドを変更する

デフォルトのエラーメッセージ

デフォルトのunittestでの長い文字列同士の比較の際のエラーメッセージは以下のようなもの。

======================================================================
FAIL: test_it (__main__.Tests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "qr_43059JDo.py", line 35, in test_it
    self.assertEqual(self.ok, self.ng)
AssertionError: '* one\n* two\n* three\n' != '* one\n* twwo\n* three\n'
  * one
- * two
+ * twwo
?    +
  * three

比較結果を見て差分がわからないわけではないけれど。独自の形式になっていて見づらいような気がする。できれば見慣れたdiffの形式で表示されて欲しい。例えば以下の様な形で。

--- first
+++ second
@@ -1,3 +1,3 @@
 * one
-* two
+* twwo
 * three

このような形式でエラーメッセージが表示されるようにしたい。

特別な assertion メソッドを作る

特別な assertion メソッドを作ればとりあえず問題は解決する。

difflib

pythonの標準ライブラリにはdifflibというライブラリがある。diffの出力を生成するライブラリなので簡単なdiffの出力をするだけなら標準ライブラリの範囲で賄える。

このライブラリはunittestの内部でも使われているが difflib.ndiff() の方が使われている。おそらくgitなどのdiffなどで見慣れているdiffの形式は difflib.unified_diff() の方だと思う。なのでこちらを使うように変更する。

ndiffの出力

  * one
- * two
+ * twwo
?    +
  * bar

unified_diffの出力

--- first
+++ second
@@ -1,3 +1,3 @@
 * one
-* two
+* twwo
 * bar

特別なassertion メソッドの asertDiff() を作成

difflib.unified_diff() を使った assertDiff() を定義してあげれば良い。

import unittest
import difflib


class DiffTestCase(unittest.TestCase):
    def assertDiff(self, first, second, msg=None):
        self.assertIsInstance(first, str, 'First argument is not a string')
        self.assertIsInstance(second, str, 'Second argument is not a string')

        msg = msg or "{} != {}".format(repr(first)[:40], repr(second)[:40])

        if first != second:
            # don't use difflib if the strings are too long
            if (len(first) > self._diffThreshold or len(second) > self._diffThreshold):
                self._baseAssertEqual(first, second, msg)

            firstlines = first.splitlines(keepends=True)
            secondlines = second.splitlines(keepends=True)
            if not firstlines[-1].endswith("\n"):
                firstlines[-1] = firstlines[-1] + "\n"
            if not secondlines[-1].endswith("\n"):
                secondlines[-1] = secondlines[-1] + "\n"

            diff = '\n' + ''.join(difflib.unified_diff(firstlines, secondlines, fromfile="first", tofile="second"))
            raise self.fail(self._formatMessage(diff, msg))


class Tests(DiffTestCase):
    ok = """\
* one
* two
* three
"""
    ng = """\
* one
* twwo
* three
"""

    def test_it(self):
        self.assertEqual(self.ok, self.ng)

    def test_diff(self):
        self.assertDiff(self.ok, self.ng)

出力結果

FF
======================================================================
FAIL: test_diff (__main__.Tests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "qr_43059WUi.py", line 42, in test_diff
    self.assertDiff(self.ok, self.ng)
  File "qr_43059WUi.py", line 23, in assertDiff
    raise self.fail(self._formatMessage(diff, msg))
AssertionError: '* one\n* two\n* three\n' != '* one\n* twwo\n* three\n' : 
--- first
+++ second
@@ -1,3 +1,3 @@
 * one
-* two
+* twwo
 * three


======================================================================
FAIL: test_it (__main__.Tests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "qr_43059WUi.py", line 39, in test_it
    self.assertEqual(self.ok, self.ng)
AssertionError: '* one\n* two\n* three\n' != '* one\n* twwo\n* three\n'
  * one
- * two
+ * twwo
?    +
  * three


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=2)

unified_diffを使ったエラーメッセージに変わった(とは言え、stack traceが2行になっているのが少し気になるかもしれない)。

assertEqual() で呼び出されるメソッドを変更する

先程定義した assertDiff() を使うように書き換えるというのでも良いけれど。どうせならば assertEqual() を使った時の出力結果が変わった方が嬉しいのではないかと思う。その方法について書く。

unittestのエラーメッセージの生成の仕組み

unittest.TestCaseassert.Equal() 自体は内部的には型に応じたassertion メソッドを適宜呼び分けているだけだったりする。

具体的には以下の様な _getAssertEqualityFunction() が呼ばれる

    def _getAssertEqualityFunc(self, first, second):
        """Get a detailed comparison function for the types of the two args.

        Returns: A callable accepting (first, second, msg=None) that will
        raise a failure exception if first != second with a useful human
        readable error message for those types.
        """
        #
        # NOTE(gregory.p.smith): I considered isinstance(first, type(second))
        # and vice versa.  I opted for the conservative approach in case
        # subclasses are not intended to be compared in detail to their super
        # class instances using a type equality func.  This means testing
        # subtypes won't automagically use the detailed comparison.  Callers
        # should use their type specific assertSpamEqual method to compare
        # subclasses if the detailed comparison is desired and appropriate.
        # See the discussion in http://bugs.python.org/issue2578.
        #
        if type(first) is type(second):
            asserter = self._type_equality_funcs.get(type(first))
            if asserter is not None:
                if isinstance(asserter, str):
                    asserter = getattr(self, asserter)
                return asserter

        return self._baseAssertEqual

ここから分かる通り、内部的には self._type_equality_funcs に型ごとに使われる assersion method が格納されている。この関連の設定自体は unittest.TestCase__init__() で行われている。

    def __init__(self, methodName='runTest'):
# .. snip
        # Map types to custom assertEqual functions that will compare
        # instances of said type in more detail to generate a more useful
        # error message.
        self._type_equality_funcs = {}
        self.addTypeEqualityFunc(dict, 'assertDictEqual')
        self.addTypeEqualityFunc(list, 'assertListEqual')
        self.addTypeEqualityFunc(tuple, 'assertTupleEqual')
        self.addTypeEqualityFunc(set, 'assertSetEqual')
        self.addTypeEqualityFunc(frozenset, 'assertSetEqual')
        self.addTypeEqualityFunc(str, 'assertMultiLineEqual')

    def addTypeEqualityFunc(self, typeobj, function):
## .. snip
        self._type_equality_funcs[typeobj] = function

assertEqual()assertDiff() が呼ばれるようなクラスを作成

以下のようにinitで追加してあげれば良い。

--- 00test.py	2016-12-24 22:16:45.000000000 +0900
+++ 01test.py	2016-12-24 22:35:16.000000000 +0900
@@ -3,6 +3,10 @@
 
 
 class DiffTestCase(unittest.TestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.addTypeEqualityFunc(str, 'assertDiff')
+

assertEqual() を使いながら difflib.unified_diff() を使った結果になっている。そしてうれしいことにtracebackも1行になっている。

test

class Tests(DiffTestCase):
    ok = """\
* one
* two
* three
"""
    ng = """\
* one
* twwo
* three
"""

    def test_it(self):
        self.assertEqual(self.ok, self.ng)

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

実行結果

F
======================================================================
FAIL: test_it (__main__.Tests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "qr_43059wou.py", line 43, in test_it
    self.assertEqual(self.ok, self.ng)
AssertionError: '* one\n* two\n* three\n' != '* one\n* twwo\n* three\n' : 
--- first
+++ second
@@ -1,3 +1,3 @@
 * one
-* two
+* twwo
 * three


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

付録: difflibの出力確認

比較した文章は以下のようなものだった。

first

* one
* two
* three

second

* one
* twwo
* three

difflibのndiffとunified_diffの出力結果の確認は以下の様なコードでできる。

import difflib

first = """\
* one
* two
* bar
""".splitlines(keepends=True)

second = """\
* one
* twwo
* bar
""".splitlines(keepends=True)

print("".join(difflib.ndiff(first, second)))
print("**")
print("".join(difflib.unified_diff(first, second, fromfile="first", tofile="second")))
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?