今、テストコードを書いている。前職ではテストコードを書く文化が無かったのでこれが初めて。ユニットテストフレームワークは unittest
を使っている。
これは初めてユニットテストを書いた男が数時間同じ箇所で嵌り続けたという話だ。格好悪い。
まず、assertRaisesとは何か。公式ドキュメントには、
assertRaises(exception, callable, *args, **kwds)
callable を呼び出した時に例外が発生することをテストします。
とある。これを読んですぐにテストを書き始めた。要するにexception
にテストで検出したい例外、callable
にテスト対象の関数を書けばいいんだろ、というぐらいの勢いで。じゃあ*args
**kwargs
は何のためにあるんだよ、と今になって思う。
ここからは簡単のため、単純に除算をするだけの関数(division.py)を例としていく。
def division(dividend, divisor):
try:
answer = dividend // divisor
except ZeroDivisionError:
return None
return answer
という関数をテストしたい。除算の結果を返すだけだけど、ZeroDivisionError
ではNone
を返す。というわけでテストを書いた。余談だけど_call_fut()
でテスト対象関数を呼び出す方法は@aodagさんの効果的なunittest - または、callFUTの秘密で学んだ。
import unittest
class TestDivision(unittest.TestCase):
def _call_fut(self, *args):
from division import division
return division(*args)
def test_valid_answer(self):
actual = 5
self.assertEqual(actual, self._call_fut(25, 5))
def test_zero_division_error(self):
self.assertRaises(ZeroDivisionError, self._call_fut(25, 0))
では、こいつを実行するとどうなるか。
assertRaises_practice/tests/test_division.py:16: DeprecationWarning: callable is None
self.assertRaises(ZeroDivisionError, self._call_fut(25, 0))
こうなる。callableがNoneだぞ
と言っている。このcallable
がわからずに2時間くらい嵌り続けた。テスト対象関数の戻り値をNone
以外にもしたし、raise
させてみたりもした。関数の戻り値をraise
にすると、
ZeroDivisionError: division by zero
こうなる。ZeroDivisionError
が発生することを確認するテストがZeroDivisionError
で落ちる。これはひどい。
ここらの足掻きをひたすら書くのも冗長に過ぎるので、先輩方に助けによって得た学びを以て、一足飛びに結論に行く。
実は先のZeroDivisionError
になってしまう件によって、どこがおかしいか気付きやすくなった。例外で落ちるということはassertRaises()
の中で呼び出されて欲しいdivision(25, 0)
が先に実行されているということになる。そしてassertRaises()
が失敗するということはこれはcallable
ではない。オブジェクトが呼び出し可能オブジェクト (=callable)
であるか判定してくれる組み込み関数callable
で確認してみる。
>>> def division(divided, divisor):
... try:
... answer = divided/divisor
... except ZeroDivisionError:
... raise
... return answer
>>>
>>> callable(division(25,1))
False
(エラーが出てしまうため引数は変えている)
division(25,1)
はcallable
ではないことがわかった。公式ドキュメントを見てみよう。
なお、クラスは呼び出し可能 (クラスを呼び出すと新しいインスタンスを返します) です。また、インスタンスはクラスが __call__() メソッドを持つなら呼び出し可能です。
とある。つまりクラスではないdivision(25, 1) = float型のオブジェクト
は__call__()
メソッドを持っていないということだろうか。float型のインスタンスであること、__call__()
を持たないことを
確認してみる。
>>> answer = division(25, 1)
>>> isinstance(answer, float)
True
>>> '__call__' in dir(answer)
False
これで間違いがはっきりした。assertRaises()
には、テストしたい関数を引数を与えた状態で入れてはいけない。
ではどうすればいいのか。俺はどうしてもdivision()
をテストしたいんだ。ならば引数を渡さない形にするとどうだろう。
>>> callable(division)
True
ようやくたどり着いた。callable
には関数を入れるべきだった。正しいアサーションはこうなる。
self.assertRaises(ZeroDivisionError, self._call_fut, 25, 0)
ここまで来るのに随分掛かった……。
他のアサーションではself.assertEqual(1, self._call_fut(5, 5))
のようにメソッドの中に戻り値を入れるけど、assertRaises()
の引数callable
は先に実行されるとテストメソッドが例外で落ちてしまう。つまり、assertRaises()
が実行されてから、引数callable
を実行してほしいからこうなるということ?
最後に念の為、division
の有効な属性を確認しておく。
>>> dir(division)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
__call__
と__class__
があるけど、クラスとして呼び出し可能なのか、__call__()
メソッドを持つインスタンスとして呼び出し可能なのか……。謎が残ってしまった。