Python Tips: unittest で例外のテストをしたい

今回は 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.TestCaseassertRaises() メソッドを使用します。 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() は次のように書いても( ValueErrorException に変更しても)成功します。

def test_invalid_key(self):
        with self.assertRaises(Exception):
            periodic_table('Ubn')

なぜなら、 assertRaises() は通常の tryexcept の仕組みと同じように、例外クラスの継承ツリーにおける親クラスは子孫の例外クラスも捕捉するようになっているためです。

つまり、 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 に渡したい引数を渡します。結果はコンテキストマネージャパターンと同じになります。

参考