2018/11/10

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

今回は Python の標準ライブラリのひとつ unittest で例外関係の部分をテストする方法についてまとめてみました。

対象の Python のバージョンは Python 3.7 です。

import unittest

例外を含むパターンの説明に入る前に、かんたんに unittest の基本の使い方を見ておきましょう。 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.TestCase を継承したクラス TestXXX を作成し、名前が test で始まる名前のメソッドを作成しその中にテストケースを書きます。すると、各メソッドが独立したテストケースとなります。

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

本当にざっくりとだけですが、テストの書き方と実行方法を押さえました(以下のコードを手元で実行したい方等はご参考にしてください)。続いて本題である例外のテストの仕方を見ていきます。

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

以上です。

参考

0 件のコメント: