今回は Python の標準ライブラリである unittest
で例外関係の処理をテストする方法についてです。
import unittest
動作確認した Python のバージョンは Python 3.7 です。
例外を含むパターンの説明に入る前に、かんたんに unittest
の基本的な使い方を見ておきましょう。
test_sample.py
:
import unittest
class TestRange(unittest.TestCase):
def setUp(self):
super().setUp()
def tearDown(self):
super().tearDown()
def test_length(self):
self.assertEqual(len(range(5)), 5)
def test_min_max(self):
self.assertIn(0, range(10))
self.assertIn(9, range(10))
self.assertNotIn(10, range(10))
unittest
を利用するには、 unittest.TestCase
を継承したクラスを作成し(名前は TestXXX
とする)、 test
で始まる名前のメソッドの中にテストケースを書きます。すると、各メソッドが独立したテストケースとして認識されます。
また、 setUp()
と tearDown()
という名前のメソッドが書くテストケースの前後に実行されます。 setUp()
は事前に、 tearDown()
は事後に実行されます。
テストケースが書かれたファイルを実行するにはコマンド python -m unittest
を使用します。
python -m unittest test_sample.py
上の test_sample.py
に対して実行すると次のような出力が表示されます。
python -m unittest test_sample.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
デフォルトではテストケースの成功 1 件につき .
が 1 つ出力されます。
-v
オプションをつけるとどのテストが実行されたか確認できます。
python -m unittest -v test_sample.py
test_length (test_exception.TestRange) ... ok
test_min_max (test_exception.TestRange) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
クラス単位・メソッド単位で絞り込んで個別のテストだけを実行することも可能です。
python -m unittest test_exception.TestRange.test_length
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
名前に特定の文字列を含むテストだけを絞って実行することもできます。
python -m unittest test_exception -k min_max
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
本当にざっくりとだけですが、これが unittest
のテストの書き方と実行方法です。続いて本題である例外のテストの仕方について見ていきます。
1. 特定の例外があがることをテストする
特定の例外があがることをテストしたいときは unittest.TestCase
の assertRaises()
メソッドを使用します。 assertRaises()
はコンテキストマネージャとして作られているので、 with
キーワードとともに使用します。
import unittest
def periodic_table(sign):
amap = {
'H': '水', 'He': '兵',
'Li': 'リー', 'Be': 'ベ',
'B': 'ぼ', 'C': 'く',
'N': 'の', 'O': 'ぉ',
'F': 'ふ', 'Ne': 'ね',
}
try:
return amap[sign]
except KeyError:
raise ValueError('不正な元素記号が指定されました: {} 。'.format(sign))
class TestPeriodicTable(unittest.TestCase):
def test_invalid_key(self):
with self.assertRaises(ValueError):
periodic_table('Ubn')
テスト test_invalid_key()
では、関数 periodic_table()
に不正な値を与えると例外が上がることをチェックしています。
このテストを上の python -m unittest
で走らせると無事成功することが確認できます。
ちなみに、 test_invalid_key()
は次のように書いても( ValueError
を Exception
に変更しても)成功します。
def test_invalid_key(self):
with self.assertRaises(Exception):
periodic_table('Ubn')
なぜなら、 assertRaises()
は通常の try
〜 except
の仕組みと同じように、例外クラスの継承ツリーにおける親クラスは子孫の例外クラスも捕捉するようになっているためです。
つまり、 assertRaises()
に Exception
のような大きめの例外を渡すと、想定と異なる例外があがったのにそれをキャッチし、本来失敗すべきケースを成功とみなしてしまうことがあります。そのため、 assertRaises()
には適切な粒度の例外を渡さなくてはなりません。
2. 例外のメッセージも含めてテストする
例外のメッセージも含めてテストするには assertRaises()
の msg
引数を使用します(ちなみに、 msg
引数は Python 3.3 で追加されたので、 Python 3.2 以前では使用することができません)。
class TestPeriodicTable(unittest.TestCase):
def test_invalid_key(self):
with self.assertRaises(ValueError, msg='不正な元素記号が選択され'):
periodic_table('Ubn')
msg
は完全なメッセージである必要はありません。 msg
が例外のメッセージの中に含まれていればテストはパスしたものとみなされます。
例外をメッセージも含めてテストする方法は他にもあります。そのひとつは、コンテキストマネージャ変数を使う方法です。
class TestSillyCase(unittest.TestCase):
def test_silly_check(self):
with self.assertRaises(NotImplementedError) as cm:
raise NotImplementedError('実装してから呼ぶのじゃ')
self.assertIsInstance(cm.exception, NotImplementedError)
self.assertEqual(cm.exception.args[0], '実装してから呼ぶのじゃ')
コンテキストマネージャ変数(この場合は cm
)にはアトリビュート exception
がありキャッチされた例外がその中に格納されています。
さらにもうひとつの方法として、 assertRaises()
の代わりに assertRaisesRegex()
メソッドを使う方法もあります。
class TestShop(unittest.TestCase):
def test_lack_of_gold(self):
with self.assertRaisesRegex(ValueError, '^手持ちは \d+ ゴールドです。 \w+ は買えません。$'):
raise ValueError('手持ちは 15 ゴールドです。 ひのきの棒 は買えません。')
assertRaisesRegex()
はその名のとおり正規表現でメッセージのチェックを行う機能を提供します。
・・・というわけで、 unittest
で例外を含むパターンをテストする方法についての説明でした。
ちなみに、 assertRaises()
・ assertRaisesRegex()
ともに、コンテキストマネージャではなく通常のメソッドとして利用することもできます。
class TestPeriodicTable(unittest.TestCase):
def test_invalid_key(self):
self.assertRaises(ValueError, periodic_table, 'Ubn')
この使い方をする場合は、第 1 引数に想定される例外クラスを、第 2 引数に対象の callable を、第 3 引数以降は第 2 引数の callable に渡したい引数を渡します。結果はコンテキストマネージャパターンと同じになります。