↓ メインコンテンツへスキップ
  1. 第03講 関数(2/2; 関数の応用)/

assert文によるテスト

テストとは
#

プログラムが期待通りに動いているかを確認するためには、実行してみて、実行結果が期待通りであるかを確認する必要があります。 この確認作業のことを テスト(test) と呼びます。 間違いのないプログラムを作成するためには、非常に多くのテストを実施する必要がありますが、テストの実施には多大な時間と労力がかかります。 そこで、プログラムのテストを自動化するために様々な方法が実施されています。 ここではそのようなテスト手法の一つである assert 文を用いたテストについて学習します。

なお、テストを効率的に実施するためには、テストしたい範囲を関数として切り出し、関数の返り値が期待通りであるかを確認することが必要です。

例題. 足し算関数のテスト
#

2つの引数を受け取り、その和を返す関数 adder を考えます。 この関数 adder をテストするには、adder関数に適当な値を渡し、その値が期待通りであるかを確認する必要があります。 以下のようなプログラムで adder関数のテストを実施できます。

def adder(x, y):
    return x + y

assert adder(1, 2) == 3, "adder(1, 2) は 3 であるべき"
assert adder(3, 4) == 7, "adder(3, 4) は 7 であるべき"
assert adder(5, 6) == 11, "adder(5, 6) は 11 であるべき"
assert adder(-1, 1) == 0, "adder(-1, 1) は 0 であるべき"
assert adder(0, -1) == 0, "このテストは失敗する。0 + -1 は -1 であるが、0であるべきという間違ったテストである。"

このように、adder の実引数に適当な値を指定して関数を呼び出します。 そして、返り値が期待通りであるかを assert 文で確認します。 assert文は、条件式が True であれば何もしませんが、False であれば AssertionError という例外を発生させます。

実行すると、以下のようになり、期待通り、一番最後のテストの失敗が確認できます。

Traceback (most recent call last):
  File "adder_test.py", line 8, in <module>
    assert adder(0, -1) == 0, "このテストは失敗する。0 + -1 は -1 であるが、0であるべきという間違ったテストである。"
           ^^^^^^^^^^^^^^^^^
AssertionError: このテストは失敗する。0 + -1 は -1 であるが、0であるべきという間違ったテストである。
テスタビリティ (testability; テストのしやすさ)

プログラムを書くときにはテスタビリティを考慮することが必要です。 テスタビリティを高めるためには、関数を小さく、独立して、再利用可能なものにすることが重要です。

一方、print 文などによるプリントデバッグは古くからよく使われてきた手法ですが、assert文との相性は非常に悪いです。 一度print文を用いて出力した内容は、assert文で確認できません。 そのため、print文で出力したい内容を関数で生成し、その関数の返り値を確認するようにすることが重要です。

assert文
#

assert文は条件式が True であれば何もしませんが、Falseであれば、AssertionError という例外を発生させます。 上では、これを利用してテストを実施しています。 これを仕込んでおくと、これまでうまくいっている部分が、 後からの変更でいつの間にか想定外の動作をしたときにいち早く気付くことができるようになります。

また、Pythonの実行時に assert 文を無効にすることもできます(-Oオプションを付けて実行すると assert文が無視される)。 そのため、処理を関数で切り出し、どんどん assert 文を追加していくことで、テストを効率的に実施することができます。

やってみよう: テストを先に書く
#

assert 文によるテストは、関数を書いた 後 に付け足すものとは限りません。 先にテストを書いておくと、「この関数は何をすべきか」が実装の前にはっきりします。 ここでは、うるう年かどうかを判定する関数 is_leap_year を、テストを先に書いてから作ってみましょう。 ファイル名は leap_year.py とします。

うるう年の規則は次の通りです。

  1. 4 で割り切れる年はうるう年である。
  2. ただし、100 で割り切れる年はうるう年ではない。
  3. ただし、400 で割り切れる年はうるう年である。

1. テストを書く
#

まず、関数の中身は pass のままにして、規則から assert 文を書きます。 規則 1〜3 のそれぞれが正しく扱われているかを確かめられるように、次の年を必ず含めてください。

  • 2024 年(4 で割り切れる → うるう年)
  • 2023 年(4 で割り切れない → うるう年ではない)
  • 1900 年(100 で割り切れる → うるう年ではない)
  • 2000 年(400 で割り切れる → うるう年)
def is_leap_year(year):
    pass

assert is_leap_year(2024) == True, "2024 年は 4 で割り切れるのでうるう年"
# 2023 年、1900 年、2000 年のテストを追加する。

この状態で実行すると、is_leap_year は None を返すので、最初の assert で失敗します。 テストが失敗することを先に確かめる のも大事な手順です。テストが間違っていて、何を書いても通ってしまうことがあるからです。

2. 関数を実装する
#

pass を消して、規則の通りに if 文で実装してください。 実行して何も表示されなければ、すべてのテストに通っています。

3. わざと壊してみる
#

規則 3(400 で割り切れる年)の部分を消して実行してみてください。 2000 年のテストだけが失敗し、次のようなメッセージが表示されます。

AssertionError: 2000 年は 400 で割り切れるのでうるう年

assert 文のメッセージには、このように「何が期待されていたか」を書いておくと、どのテストがなぜ失敗したのかがすぐに分かります。 確認したら、規則 3 を元に戻しておいてください。

🏁 解答例
def is_leap_year(year):
    if year % 400 == 0:      # 規則 3: 400 で割り切れる年はうるう年。
        return True
    if year % 100 == 0:      # 規則 2: 100 で割り切れる年はうるう年ではない。
        return False
    return year % 4 == 0     # 規則 1: 4 で割り切れる年はうるう年。

assert is_leap_year(2024) == True,  "2024 年は 4 で割り切れるのでうるう年"
assert is_leap_year(2023) == False, "2023 年は 4 で割り切れないのでうるう年ではない"
assert is_leap_year(1900) == False, "1900 年は 100 で割り切れるのでうるう年ではない"
assert is_leap_year(2000) == True,  "2000 年は 400 で割り切れるのでうるう年"

規則は 3 → 2 → 1 の順に調べます。逆の順(先に 4 で割り切れるかを調べる)にすると、1900 年がうるう年になってしまいます。