【Python】pytestのparametrizeで境界値と入力パターンをまとめて検証する

PythonのTopに戻る

入力と期待値を並べて境界を確認する

0から100までの件数を読む関数なら、普段よく使う10だけでなく、0、100、その外側、空文字も確認したい。同じテストを何本もコピーすると、後から修正するときに期待値や対象関数を一部だけ直し忘れる。pytest.mark.parametrizeを使えば、共通の操作と入力ごとの期待を分け、ケースを一覧として管理できる。

以下の関数は「半角数字だけで書かれた0〜100の整数」を受け付ける。空白、符号、小数、全角数字は拒否する。値の範囲と表記の規則を別々の観点としてテストへ含める。先頭ゼロはこの例では認めるが、必要なら表記規則に加えて検証する。

正常値と異常値を別のパラメーター表にする

count_parser.py

import re


def parse_count(text):
    if not isinstance(text, str) or re.fullmatch(r"[0-9]+", text) is None:
        raise ValueError("count must use ASCII digits")
    value = int(text)
    if not 0 <= value <= 100:
        raise ValueError("count must be from 0 to 100")
    return value

test_counts.py

import pytest
from count_parser import parse_count


@pytest.mark.parametrize(
    "text, expected",
    [
        pytest.param("0", 0, id="lower-boundary"),
        pytest.param("1", 1, id="above-lower"),
        pytest.param("99", 99, id="below-upper"),
        pytest.param("100", 100, id="upper-boundary"),
    ],
)
def test_valid(text, expected):
    assert parse_count(text) == expected


@pytest.mark.parametrize(
    "text",
    [
        pytest.param("", id="empty"),
        pytest.param("-1", id="negative"),
        pytest.param("101", id="above-upper"),
        pytest.param(" 1", id="leading-space"),
        pytest.param("1", id="full-width"),
        pytest.param("1.0", id="decimal"),
    ],
)
def test_invalid(text):
    with pytest.raises(ValueError):
        parse_count(text)

二つのファイルを同じフォルダーに置き、python -m pytest -q test_counts.pyで実行する。実際にはテスト関数は2本だが、入力の組ごとに独立して実行されるため、正常4件と異常6件の計10件になる。次は集計部分の抜粋で、時間表示を省略した。

実行結果(抜粋)

10 passed

テスト名に理由が見えるようにする

pytest.param(…, id=…)で各ケースへ意味の分かる名前を付けた。失敗したときにupper-boundaryと表示されれば、上限を含む条件が怪しいと分かる。単にcase1、case2とするより、どの仕様を確認しているのかを名前へ残すと役立つ。IDは期待値の代わりにはならないので、値も正しく設定する。

parametrizeの最初の文字列に並べた名前が、テスト関数の引数へ対応する。正常系ではtextとexpectedの組を渡し、異常系ではtextだけを渡す。入力数と引数数が合わないなどの定義ミスは、関数の実行前の収集段階で問題になる。テストが実行されていないのに、対象関数の不具合だと思い込まないようにする。

各ケースは独立したテスト項目として扱われる。普通のforループで同じassertを並べると、最初の失敗で残りを見られない場合があるが、パラメーター化するとどの入力が成功し、どれが失敗したかを確認しやすい。必要なら-vで個別の名前を表示し、特定のIDに関連するケースへ絞って調べられる。

境界値は仕様から選ぶ

0と100が許可範囲に含まれること、101が外れることを確認している。下限の外側である-1は、この例では値の範囲へ進む前に表記で拒否される。数値範囲の分岐そのものを細かくテストしたい場合は、整数を受け取る検証関数と文字列を読む関数を分ける設計もある。

空文字と全角数字は、数値の大小ではなく表記の条件を確かめるケースである。多数のランダムな数を並べるだけでは、こうした種類の違いを確認できない。境界、空、型違い、特殊な表記など、条件ごとの代表例を考えてから数を増やす方が有効である。

期待値を対象関数と同じアルゴリズムで自動生成すると、両方が同じ誤りを持っていてテストが通ることがある。今回のような小さなケースは、仕様から期待値を明示する方が分かりやすい。仕様を変更した場合は、どのケースの期待が変わるのかをレビューする。

ケースのデータを変更しない

パラメーターとして渡したリストや辞書は、pytestが毎回深いコピーを作るわけではない。テスト中に変更すると、同じオブジェクトを共有する別ケースへ影響する可能性がある。変更する必要がある入力はケースごとに生成するかコピーし、準備が複雑ならfixtureへ分ける。この記事では文字列と整数だけなので、その問題を避けている。

パラメーター化を何段も組み合わせると、ケース数が組み合わせの積で増える。実行時間だけでなく、失敗時に何を確認しているのかも見えにくくなるため、必要な組み合わせと代表例を選ぶ。数を増やすことより、受け入れる条件と拒否する条件が明確に並んでいることを優先するとよい。

実行環境と関連情報

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

関連:IDの形式を厳密にチェックする:fullmatchとASCII文字の指定 / pytestで正常系と異常系の自動テストを始める

PythonのTopに戻る