パラメタライズって何?まずはイメージから
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
Pythonpytest -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
Pythonpytest -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 つのテスト関数を「データの数だけ」自動的に展開してくれる。
入力と期待値だけが違う同じ形のテストを、コピペせずに「テストデータの表」として書けるようになる。
失敗したときも、どのパラメータの組み合わせで落ちたかが分かりやすく表示される。
「同じテストをパターン違いで何個も書き始めたら」、それはパラメタライズにするサイン。
