見出し画像

pythonプログラム初歩の初歩10/作った関数をチェックしよう

こんにちはmakokonです。プログラムしていますか?
今日は自作関数の単体テストの話です。
プログラムを書くときには、なるべく処理が冗長にならないように関数化して記載するものですが、実際にはこの関数が正しいことを確認するのはけっこう大変です。
makokonは、恥ずかしながら実際に関数の中にprintを大量に埋め込んでいちいち、その挙動を確認していました。
テスト用データも作成するのですが、実システム(最終アプリ)の中に埋め込んでしまうと煩雑になるし、関数単体で検証することもなかなかできません。
そこで知ったのが、unittestライブラリです。pythonを主力に使うようになって、もう1年以上になるのに、全然知りませんでした。おはずかしい。
早速マニュアルと、ChatGPTに尋ねながら試してみました。


unittestとは

  • 公式ドキュメント(日本語版)が下記にあります。
    unittest では、テストの自動化・初期設定と終了処理の共有・テストの分類・テスト実行と結果レポートの分離などの機能を提供しており、 unittest のクラスを使って簡単にたくさんのテストを開発できるようになっています。

  • テストフィクスチャ (test fixture)
    test fixture とは、テスト実行のために必要な準備や終了処理を指します。例: テスト用データベースの作成・ディレクトリ・サーバプロセスの起動など。

  • テストケース (test case)
    test case はテストの最小単位で、各入力に対する結果をチェックします。テストケースを作成する場合は、 unittest が提供する TestCase クラスを基底クラスとして利用することができます。

  • テストスイート (test suite)
    test suite はテストケースとテストスイートの集まりで、同時に実行しなければならないテストをまとめる場合に使用します。

  • テストランナー (test runner)
    test runner はテストの実行と結果表示を管理するコンポーネントです。ランナーはグラフィカルインターフェースでもテキストインターフェースでも良いですし、何も表示せずにテスト結果を示す値を返すだけの場合もあります。

詳細は、ドキュメントを読んでもらうとして、今回は基本的な使い方を示します。初歩の初歩ですからね。

テスト用プログラムの用意

今回は、簡単な割り算プログラムを用意します。(division.py)
このプログラムは、2数a,bを与えると、その結果を[商,余り]というリストで返します。

# division.py
def division(a, b):
    """aをbで割った商と余りをリストで返す"""
    quotient = a // b
    remainder = a % b
    return [quotient, remainder]

if __name__ == '__main__':
    result=division(10,3)
    print(f"10 / 3 = {result[0]} 余り {result[1]}")

実行結果
$python division.py
10 / 3 = 3 余り 1

a=10.b=3のとき、「 10 / 3 = 3 余り 1」と正しく表示されます。
まあ、この程度なら間違ってるわけないので、これでもいいのですが、はじめの一歩ですからこれをちゃんとテストしましょう

テストプログラム

先程のdivision.pyをテストするプログラム(lesson10.py)を書きます。
a,bの組み合わせですが、normalテストとして、余りが出ない組み合わせと、あまりが出る組み合わせを用意します。ここで大切なのは期待される結果を用意しておくことです。
また、0割のときに、エラがー出るかどうかをチェックします。
もしかすると、処理系によって、0で割り算してもエラーを返さない事があるかもしれません。

# lesson10.py for test_add.py
import unittest
from division import division

class TestDivisionFunction(unittest.TestCase):
    def test_division_normal(self):
        self.assertEqual(division(10, 2), [5, 0])  # 通常の割り算
        self.assertEqual(division(10, 3), [3, 1])  # 余りが出る割り算

    def test_division_zero(self):
        # ゼロによる割り算を試みるときのエラーチェック
        with self.assertRaises(ZeroDivisionError):
            division(10, 0)

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

実行結果
$python lesson10.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

この結果、2つのテストが実行され、いずれも成功したことがわかります。

コードの説明

簡単に説明しますね。

from division import division

 これはdivision.pyに含まれるdivision関数を利用できるようにします。このときdivision.pyは、テストプログラムと同じディレクトリにあるか、パスを通したディレクトリにある必要があります。
(実際には、いくつかの別ディレクトリを利用する方法がありますが、詳細はまた調べてみてください。[sys.path二追加、環境変数PYTHONPATHを使用、相対pathを利用、パッケージ化])

class TestDivisionFunction(unittest.TestCase):


この中に、テスト関数を記載します。"test"で始まる関数は自動的に実行されてそのそれぞれが成功したか失敗したかを確認できます。

def test_division_normal(self):


この使い方が、最も多いと思います。関数の呼び出しと、その正解を渡してチェックします。division(10,2)に対する正しい出力は[5,0]ですね。

def test_division_zero(self):
  self.assertRaises(ZeroDivisionError):


これは、指定したエラーが発生することを期待するもので、この場合は0除算のエラーが発生すれば成功、発生しなければ失敗になります。
実システムでエラーが発生する条件を追跡するのは大変なのですが、エラーになる条件を確認できるいい方法ですね。
他にも、pythonだと以下のようなエラータイプがサポートされています。

  • ValueError:不適切な値が関数に渡された場合に発生します。

  • TypeError:不適切な型のオブジェクトが操作または関数に渡された場合に発生します。

  • IndexError:シーケンスの範囲外のインデックスにアクセスしようとした場合に発生します。

  • KeyError:辞書に存在しないキーにアクセスしようとした場合に発生します。

  • FileNotFoundError:存在しないファイルまたはディレクトリにアクセスしようとした場合に発生します。

これらのテストでも、with self.assertRaises(例外名):の形式でチェックすることができます。

unittest.main()


テストを実行します。定義されたtestXXXX関数を順番にチェックし、
成功すれば'.'を失敗すれば、'F'を表示します。最後に2つのtestを実行したことが表示されます。

関数が間違っていたときの表示

テスト関数が簡単なので、テストが成功してしまいましたが、失敗したときの表示も確認しておきましょう
間違ったdivision.pyを作ってもいいですが、今回は間違ったテストデータで確認してみます。(lesson10-1.py)

# lesson10-1.py for test_add.py
import unittest
from division import division

class TestDivisionFunction(unittest.TestCase):
    def test_division_normal(self):
        self.assertEqual(division(10, 2), [5, 0])  # 通常の割り算
        self.assertEqual(division(10, 3), [3, 1])  # 余りが出る割り算
        self.assertEqual(division(10, 3), [4, 1])  # 間違ったテストデータ

if __name__ == '__main__':
    unittest.main()
実行結果
$python lesson10-1.py
F
======================================================================
FAIL: test_division_normal (__main__.TestDivisionFunction.test_division_normal)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "lesson10-1.py", line 9, in test_division_normal
    self.assertEqual(division(10, 3), [4, 1])  # 間違ったテストデータ
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Lists differ: [3, 1] != [4, 1]

First differing element 0:
3
4

- [3, 1]
?  ^

+ [4, 1]
?  ^


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

実際に実行するとまずFが表示されてテストが失敗だったことが示されます。つづけて、テストが失敗したコードと、想定結果と出力結果の違いが詳細に示されます。
そして、最終的に1つのテストが実行され失敗に終わったメッセージが表示されます。

このように、テストが失敗したときにどんなデータで、どんな違いが出たかが詳細にわかるので、とても検討しやすいです。ただし、注意が必要なのは、今回がまさにそうなのですが、テストがしっぴした原因が関数にあるのかテストデータにあるのかは区別することができません。テストが上手くいかないときには、テストデータの正当性も再チェックすることが重要ですね。

まとめ

以上、今回はunittestを使った、関数の単体テストの基礎を紹介しました。
作成した関数の出力結果とテストデータの照合、特殊な条件でのエラー発生の確認など、とても有用な試験ライブラリでした。
みなさんもぜひ、作成したプログラムをユニットごとに検証する習慣をつけていただきたいと思います。(自分のことを棚に上げている)
また、unittestには、紹介した以外にも便利な使い方がたくさんあります。勉強中ですが、別の機会に紹介できればと思います。

#python #program #初歩 #単体テスト #unittest #エラーチェック
#初歩の初歩10

おまけ タイトル画の説明 by GPT-4V


この画像には、伝統的な教授のスタイルを身にまとった男性が黒板の前に立っています。黒板には「UNIT TESTS」と題された数学的な式やグラフが書かれており、彼は指導中のように見えます。教授は時代を感じさせる眼鏡をかけ、分厚い灰色の口ひげ、白い髪を持ち、茶色のツイードのジャケット、ネクタイ、そしてシャツを着ています。彼の姿勢や表情からは経験の豊かさと、自信が感じられます。教室の雰囲気は学術的で、教授は情熱を持って教えている様子です。


この記事が気に入ったらサポートをしてみませんか?