はじめに
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.TestCase
の assert.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")))