【Python】pytestで正常系と異常系の自動テストを始める

PythonのTopに戻る

目で見ていた確認を何度でも実行できる形にする

関数を修正するたびにprintの出力を目で比較しているなら、pytestで期待値と期待する例外を記録すると、同じ確認を繰り返せる。まず正常な入力で値が合うこと、次に不正な入力で決めた例外になることをテストする。例外が出ないことだけを確認しても、間違った値を静かに返す不具合は見つからない。

以下の3ファイルを同じフォルダーへ保存する。計算する関数とテストを分け、通常の成功するテストと、失敗表示を読むための意図的な誤ったテストも別ファイルにする。後者を含めて全体が成功したとは扱わない。掲載例はpytest 9.1.1で実際に実行している。pytestを導入する場合は、自分の作業用仮想環境へ入れて実行する。

正常値とValueErrorをテストする

stats_tools.py

def mean(values):
    if not values:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

test_stats.py

import pytest
from stats_tools import mean


def test_mean_normal():
    assert mean([1, 3]) == 2.0


def test_mean_empty():
    with pytest.raises(ValueError, match="must not be empty"):
        mean([])

このフォルダーでpython -m pytest -q test_stats.pyを実行する。以下は実行結果の集計部分の抜粋で、経過時間の表示は省略したもの。2件とも成功し、終了コードは0だった。

実行結果(抜粋)

2 passed

意図的に失敗するテストから表示を読む

test_wrong.py

from stats_tools import mean


def test_wrong_expected_value():
    assert mean([1, 3]) == 3.0

こちらは期待値をわざと3.0にした誤ったテストである。python -m pytest -q test_wrong.py –tb=shortとして実行すると1件失敗し、終了コードは1になる。次は比較内容の抜粋である。関数の計算結果2.0と、テストに書いた期待値3.0が一致していない。

実行結果(抜粋)

E   assert 2.0 == 3.0
E    +  where 2.0 = mean([1, 3])

assertとraisesの役割

pytestはtest_で始まる関数を見つけて実行し、assertの比較に失敗するとその内容を表示する。どのファイルのどのテストが失敗したかを先に見て、期待値と実際の値を比較する。失敗したからといって、すぐ期待値を実際の値へ書き換えるのではなく、仕様に照らして関数とテストのどちらが間違っているか判断する。

pytest.raises()のwithの中では、指定した例外が起きることが期待される。例外が出なければ失敗し、別の種類ならその問題が分かる。matchは例外メッセージに対する正規表現なので、特殊記号を含む文字列をそのまま書くと意味が変わる場合がある。例では分かりやすい固定の語句で確認している。

raisesの範囲は、例外が出るはずの一つの操作へ絞る。準備処理までwithへ入れると、準備の失敗を目的の例外として誤って受け入れることがある。今回のmean([])は空入力を明示的に拒否する契約なので、その関数呼び出しだけを囲んでいる。

実行するテストを明示する

python -m pytestを使うと、そのpythonに対応したpytestで実行できる。別の仮想環境のpytestコマンドを誤って呼ぶ問題を調べる手がかりにもなる。ファイルを指定せず実行するとtest_wrong.pyも収集されるため、この教材の全テストは意図どおり1件失敗する。普段の回帰テストへ残すのは正しい期待値に直したテストである。

assertは通常のPythonでは最適化オプションで省略されうるため、利用者入力の必須検証をassertだけで書かない。例のmean()はifとraiseで空入力を確認し、テスト側でassertを使っている。実行時の入力検証と開発時の自動確認を分けると、役割がはっきりする。

小数を扱う関数では、浮動小数点の丸めを考えてpytest.approx()などを使う場面もある。どんな結果にも適当な許容誤差を付けるのではなく、値の意味と必要な精度から決める。ここでは正確に表現できる2.0を使い、テストの始め方に集中している。

最初のテストを増やすとき

実際に見つかった不具合は、再現する最小入力を追加してから修正すると、直ったかを確かめやすい。正常例だけを増やすより、空、1件、境界値、期待する例外をそろえると取りこぼしを減らせる。外部サービスや実データに依存する部分は、小さな関数へ分けたりテスト専用の入力を用意したりして、毎回同じ条件で確認できるようにする。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。pytestを使う例はpytest 9.1.1で確認している。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:pytestのparametrizeで境界値と入力パターンをまとめて検証する / fixtureとtmp_pathで実データを壊さずファイル処理をテストする / doctestで説明文のコード例が古くなるのを防ぐ

PythonのTopに戻る