Python | テスト・設計・品質:パラメタライズ

Python Python
スポンサーリンク

パラメタライズって何?まずはイメージから

pytest の「パラメタライズ(parametrize)」は、
「同じテストの形で、入力と期待値だけを変えて、何パターンも一気にテストする仕組み」です。

同じ関数に対して、

1 + 2 は 3
0 + 5 は 5
-1 + 3 は 2

みたいに「パターンだけ違うテスト」を、
1 個ずつテスト関数を書くのではなく、
1 個のテスト関数に「テストデータの一覧」を渡して展開してもらうイメージです。

「テストのコピペ地獄」を避けるための、かなり強力な道具です。


まずは素朴なテストから始めてみる

パラメタライズなしのテスト

足し算関数を例にします。

# calc.py
def add(a: int, b: int) -> int:
    return a + b
Python

これをテストするとき、パラメタライズを使わないとこうなりがちです。

# test_calc.py
from calc import add

def test_add_simple():
    assert add(1, 2) == 3

def test_add_zero():
    assert add(0, 5) == 5

def test_add_negative():
    assert add(-1, 3) == 2
Python

悪くはないですが、「同じ形のテスト」が増えていきます。
入力と期待値だけ違って、書き方は全部同じですよね。

ここを「1 本のテスト関数+テストデータの一覧」にまとめるのが、パラメタライズです。


pytest.mark.parametrize の基本形

1 つの引数をパラメタライズする

まずは、引数が 1 つだけのシンプルな例から。

import pytest
from calc import add

@pytest.mark.parametrize("x", [1, 2, 3])
def test_x_is_positive(x):
    assert x > 0
Python

ここで起きていることは、

parametrize("x", [1, 2, 3]) と書く
pytest がこのテストを 3 回実行する
1 回目は x=1、2 回目は x=2、3 回目は x=3

という動きです。

テスト関数は 1 個ですが、
実行されるテストケースは 3 個に展開されます。

複数の引数をパラメタライズする(これが本命)

足し算のテストをパラメタライズすると、こうなります。

import pytest
from calc import add

@pytest.mark.parametrize(
    "a, b, expected",
    [
        (1, 2, 3),
        (0, 5, 5),
        (-1, 3, 2),
    ],
)
def test_add(a, b, expected):
    assert add(a, b) == expected
Python

ここでのポイントは、

"a, b, expected" という 3 つの引数名を指定
その順番に対応するタプルのリストを渡す
pytest が「(1, 2, 3)」「(0, 5, 5)」「(-1, 3, 2)」を順番に代入してテストを回す

というところです。

テストの「形」は 1 個だけ。
テストデータ(入力と期待値)だけを一覧で渡している、という構造になります。


パラメタライズの何がそんなに嬉しいのか

コピペをやめて「テストデータの表」にできる

パラメタライズを使うと、
テストが「テストデータの表」に見えるようになります。

@pytest.mark.parametrize(
    "a, b, expected",
    [
        (1, 2, 3),   # 普通の足し算
        (0, 5, 5),   # 0 を含む
        (-1, 3, 2),  # 負の数を含む
    ],
)
def test_add(a, b, expected):
    ...
Python

「どんなパターンをテストしているか」が、
テスト関数の上の一覧を見るだけで分かります。

テスト関数をコピペして増やしていくと、

どこが違うのか分かりにくい
1 箇所だけ修正漏れが出る
テスト名を考えるのが面倒になる

といった問題が出てきますが、
パラメタライズなら「データを 1 行足すだけ」でパターンを増やせます。

新しいパターンを足すのがめちゃくちゃ楽になる

例えば、「大きな数の足し算もテストしたい」と思ったら、
こう 1 行足すだけです。

        (10**6, 10**6, 2 * 10**6),
Python

テスト関数を増やす必要はありません。
「テストしたいパターンをどんどん表に追加していく」感覚で、
テストの網羅性を上げていけます。


失敗したときの表示も分かりやすい

どのパターンが落ちたかが一目で分かる

わざと 1 パターンだけ期待値を間違えてみます。

@pytest.mark.parametrize(
    "a, b, expected",
    [
        (1, 2, 3),
        (0, 5, 5),
        (-1, 3, 999),  # わざと間違い
    ],
)
def test_add(a, b, expected):
    assert add(a, b) == expected
Python

pytest -v で実行すると、だいたいこんな感じになります。

test_calc.py::test_add[1-2-3] PASSED
test_calc.py::test_add[0-5-5] PASSED
test_calc.py::test_add[-1-3-999] FAILED

[a-b-expected] の部分に、
そのテストケースのパラメータが埋め込まれています。

どの組み合わせで落ちたかが一目で分かるので、
原因の特定がしやすくなります。


少し応用:ID を付けてテストケースに名前をつける

デフォルトの ID だと読みにくいとき

パラメタライズの ID([1-2-3] みたいな部分)は、
デフォルトだと「引数の値をつなげたもの」になります。

もっと意味のある名前を付けたいときは、ids 引数を使います。

@pytest.mark.parametrize(
    "a, b, expected",
    [
        (1, 2, 3),
        (0, 5, 5),
        (-1, 3, 2),
    ],
    ids=["normal", "with_zero", "with_negative"],
)
def test_add(a, b, expected):
    assert add(a, b) == expected
Python

pytest -v で見ると、こうなります。

test_calc.py::test_add[normal] PASSED
test_calc.py::test_add[with_zero] PASSED
test_calc.py::test_add[with_negative] PASSED

「このケースは何を意図しているのか」が、
ID からすぐに分かるようになります。


fixture とパラメタライズを組み合わせる

fixture をパラメタライズする

@pytest.mark.parametrize だけでなく、
fixture 自体をパラメタライズすることもできます。

import pytest

@pytest.fixture(params=[1, 2, 3])
def number(request):
    return request.param

def test_number_is_positive(number):
    assert number > 0
Python

ここでは、

number という fixture が 1, 2, 3 の 3 パターンで動く
それぞれの値がテスト関数に渡される

という形になります。

複数の fixture を組み合わせると、
パラメータの組み合わせを自動で展開してくれたりもしますが、
初心者のうちはまず @pytest.mark.parametrize の方をしっかり押さえれば十分です。


どんなときにパラメタライズを使うべきか

「同じ形のテストをコピペし始めたら」使いどき

次のような兆候が出てきたら、パラメタライズを検討するサインです。

同じテスト関数を、入力と期待値だけ変えて何個も書いている
テスト関数名に「_case1」「_case2」みたいな番号を付け始めている
「この関数、もっといろんなパターンを試したいけど、テストを書くのがダルい」と感じている

そういうときは、一度テストをこう見直してみてください。

テストの「形」は 1 個にできないか?
違うのは「入力」と「期待値」だけではないか?

そうであれば、パラメタライズにすると一気にスッキリします。


まとめ(パラメタライズは「テストを表にする技」)

pytest のパラメタライズを初心者目線で整理すると、こうなります。

@pytest.mark.parametrize("引数名リスト", テストデータのリスト) で、1 つのテスト関数を「データの数だけ」自動的に展開してくれる。
入力と期待値だけが違う同じ形のテストを、コピペせずに「テストデータの表」として書けるようになる。
失敗したときも、どのパラメータの組み合わせで落ちたかが分かりやすく表示される。
「同じテストをパターン違いで何個も書き始めたら」、それはパラメタライズにするサイン。

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