Python の勉強 スロットゲーム編 〜その17:テスト(unittest)〜

プログラミング

先述の doctest と違い、こちらは他のプログラム言語でも広く使われる一般的な単体テストになります。Smalltalk というオブジェクト指向言語の始祖みたいなプログラミング言語が実装した「SUnit」からの系譜のようです。それ依頼「xUnit」という名前が使われてるんですね〜。

xUnitとは、コンピュータプログラムの単体テスト(ユニットテスト)を行うためのテスティングフレームワークの総称である。(…略…)

このようなフレームワークの最初の実装は、ケント・ベックが開発したSmalltalk用のテスティングフレームワークSUnitである。

xUnit – Wikipedia

xUnit の構成

「xUnit」は色んなプログラミング言語で採用され切磋琢磨しているため機能が充実しています。こちらの記事で全てを確認することはできないので、doctest と同等の規模にしぼりたいと思います。以下の図でいうと、「テストクラス」を作成することになります。

「スイート」とは、「スイートルーム」や「オフィススイート」の “スイート” と同じ意味で、「一式、揃った」という意味になります。「メソッド」はクラスの中での関数を指します。「じゃ “クラス” って何?」という疑問については、”オブジェクト指向とは” みたいな話になってしまうので別途……。今はまだメソッド(関数)をまとめたもの…という感じでご了承下さい。
setUpClasstearDown もメソッドです。テストをするために必要な準備や後処理を記載します
テストクラスに記載した内容をテストランナーが読み取り、順番に実行していきます。

xUnit での結果と期待値の比較

doctype では、関数を使った際の期待値を次の行に記載しました。doctype モジュールがそれを読み取り、結果と比較して「OK or NG」を判断しました。xUnit 系では主に「assertEqual(結果, 期待値)」という書き方をします。この assertXxx には沢山の種類がありますので状況に応じて使い分けられるようになると、大変、便利です。
公式サイトには沢山 assertXxx が載っています。中には「直接呼び出す必要がないことに注意」や、「非推奨のエイリアス」という記載が混ざっていますので、参照の際はお気をつけ下さい。

“エイリアス” とは、同じ機能を持った別の書き方を指します。「assertEquals(a, b)」と書いても、「failUnlessEqual(a, b)」と書いても、「assertEquals(a, b)」と書いても同じ結果になりますが、そういう書き方は “推奨しません” と言ってます。
例えば東京の “山手線” は「やまのてせん」(正式)でも「やまてせん」(別名)でも通じるけども、「やまてせん」と呼ぶのはやめましょうというお願いと一緒です。
つまり使うべき assertXxx として記載されているのは以下になります。

最も一般的に使われるメソッド

メソッド確認事項初出
assertEqual(a, b)a == b
assertNotEqual(a, b)a != b
assertTrue(x)bool(x) is True
assertFalse(x)bool(x) is False
assertIs(a, b)a is b3.1
assertIsNot(a, b)a is not b3.1
assertIsNone(x)x is None3.1
assertIsNotNone(x)x is not None3.1
assertIn(a, b)a in b3.1
assertNotIn(a, b)a not in b3.1
assertIsInstance(a, b)isinstance(a, b)3.2
assertNotIsInstance(a, b)not isinstance(a, b)3.2
最も一般的に使われるメソッド

例外、警告、およびログメッセージの発生を確認する

メソッド確認事項初出
assertRaises(exc, fun, *args, **kwds)fun(*args, **kwds) が exc を送出する
assertRaisesRegex(exc, r, fun, *args, **kwds)fun(*args, **kwds) が exc を送出してメッセージが正規表現 r とマッチする3.1
assertWarns(warn, fun, *args, **kwds)fun(*args, **kwds) が warn を送出する3.2
assertWarnsRegex(warn, r, fun, *args, **kwds)fun(*args, **kwds) が warn を送出してメッセージが正規表現 r とマッチする3.2
assertLogs(logger, level)with ブロックが 最低 level で logger を使用する3.4
例外、警告、およびログメッセージの発生を確認する

より具体的な確認を行うため

メソッド確認事項初出
assertAlmostEqual(a, b)round(a-b, 7) == 0
assertNotAlmostEqual(a, b)round(a-b, 7) != 0
assertGreater(a, b)a > b3.1
assertGreaterEqual(a, b)a >= b3.1
assertLess(a, b)a < b3.1
assertLessEqual(a, b)a <= b3.1
assertRegex(s, r)r.search(s)3.1
assertNotRegex(s, r)not r.search(s)3.2
assertCountEqual(a, b)a and b have the same elements in the same number, regardless of their order.3.2
より具体的な確認を行うため

テストクラスを作成

以下は doctype で行ったテストを unittest で書き換えたものになります。

from io import StringIO
import unittest             # unittest 用のライブラリが必要
import unittest.mock        # print()、input() をテストするために必要

import game017              # テスト対象のモジュールが必要


class TestGame017(unittest.TestCase):         # TestXxx という名前にする決まり
                                              # setUpClass, setUp, tearDown, tearDownClass は未使用

    def test_is_bets_valid(self):             # test_xxx_xxxx という名前にする決まり
        self.assertEqual(game017.is_bets_valid('100', 100),               # 実際の結果と
                         (True, None))                                    # 期待値を比較
        self.assertEqual(game017.is_bets_valid('200', 100),
                         (False, '掛けコイン数は半角数字で入力して下さい。'))
        self.assertEqual(game017.is_bets_valid('0', 100),
                         (False, '掛けコイン数は 1 枚以上を指定して下さい。'))
        self.assertEqual(game017.is_bets_valid('101', 100),
                         (False, '掛けコイン数は所持コイン数以下を指定して下さい。'))
        with self.assertRaises(TypeError):                                # エラーが発生する場所は
            game017.is_bets_valid('100', 'abc')                           # with で囲む
    
    @unittest.mock.patch('sys.stdout', new_callable=StringIO)             # print() の結果をテストする準備
    def test_show_start_message(self, mock_stdout):
        game017.show_start_message()
        self.assertEqual(mock_stdout.getvalue(),
                        '--------------\nスロットゲーム\n--------------\n')

    @unittest.mock.patch('sys.stdout', new_callable=StringIO)             # print() の結果をテストする準備
    def test_ask_player_name(self, mock_stdout):
        with unittest.mock.patch('builtins.input', return_value="king"):  # input() を 'king' を返却するものに置き換え
            self.assertEqual(game017.ask_player_name(),
                             'king')
            self.assertEqual(mock_stdout.getvalue(),
                             'こんにちは king さん\n')


if __name__ == "__main__":
    unittest.main()

実際のテストは VS Code から簡単に実行できます。

今回は「print」や「input」がテスト対象に入っていたため多少複雑になってしまいました。基本的には test_is_bets_valid のようなテストを書いていくのがメインになると思います。

コメント

タイトルとURLをコピーしました