はじめに
Pythonのユニットテストフレームワークの1つ、pytest。
pytestはパラメタ化したテストも可能にしてくれるのですが、パラメタ化したテストについて最近知った小さいtipsをアウトプットします!
目次
- はじめに
- 目次
- 前提情報
- pytestでパラメタ化したテスト
- 💡1️⃣ クラスもデコレートできる
- 💡2️⃣ デコレータを複数積める(自動で組合せてくれる!)
- ドキュメント「How to parametrize fixtures and test functions」にはまだまだtips満載!
- 終わりに
- P.S. 積めるのに気づけたのはChatGPTのおかげ!
前提情報
今回の参考文献
- 『テスト駆動Python』2.8
- pytestのドキュメント How to parametrize fixtures and test functions
- 7.3.x系を確認しました
動作環境
- Python 3.10.9
- pytest 7.3.0
pytestでパラメタ化したテスト
@pytest.mark.parametrize
というデコレータを使うと、パラメタ化したテスト(parametrized tests)ができます3。
ドキュメントの例を見てみましょう。
import pytest @pytest.mark.parametrize( "test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)] ) def test_eval(test_input, expected): assert eval(test_input) == expected
このファイル(test_expectation.py)のあるディレクトリでpytest
コマンドを叩くと、3件のテストが実行されます。
collected 3 items test_expectation.py ..F [100%]
@pytest.mark.parametrize
によって、test_input
とexpected
の組として3パターンが渡されており、test_eval
は全部で3回実行されました。
("3+5", 8)
と("2+4", 6)
が渡されたときはテストはpass("6*9", 42)
が渡されたときはexpected
が間違っているのでテストはfail- (ドキュメントの残りの説明に使うために、わざと間違った
expected
にしているようです)
- (ドキュメントの残りの説明に使うために、わざと間違った
このように複数のパラメタ(の組)を渡して書けるのがパラメタ化されたテストです。
def_test_eval_足し算
やdef_test_eval_掛け算
のようにテストメソッドの記述を増やさずに済むのがありがたいな〜と感じています。
私の場合、テストのリファクタリングとしてパラメタ化されたテストに切り替えることが多いです。
では、参考文献で知ったtipsを紹介していきます!
💡1️⃣ クラスもデコレートできる
クラスのすべてのテストメソッドに同じデータセットを渡すことができます。(『テスト駆動Python』Kindle の位置No.1443-1444)
ドキュメントの例です。
import pytest @pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)]) class TestClass: def test_simple_case(self, n, expected): assert n + 1 == expected def test_weird_simple_case(self, n, expected): assert (n * 1) + 1 == expected
TestClass
のtest_simple_case
にもtest_weird_simple_case
にも、(n, expected)
の組として(1, 2)
と(3, 4)
が渡ります。
なので、結果として合計4件のテストが実行されます。
pytestはTest
で始まる名前のクラスのテストメソッドも見つけてくれます。
私は小さい関数やクラスが好きということもあり、クラスを入れ子にして書く4のを試しています。
その中で上のTestClass
のようなケースがありました。
クラスを@pytest.mark.parametrize
でデコレートすることで、メソッドごとに繰り返しデコレータを書かずに済んでスッキリしました!
(ドキュメントにはクラスだけでなくモジュールにも適用する例があります)
@pytest.mark.parametrize
の不思議
出来心でやってみたのですが、引数の順番を入れ替えてクラスの中で一貫性を崩しても、何も問題なく動いたぞ...5
@pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)]) class TestClass: def test_simple_case(self, n, expected): assert n + 1 == expected - def test_weird_simple_case(self, n, expected): + def test_weird_simple_case(self, expected, n): assert (n * 1) + 1 == expected
test_simple_case
は引数がn
、expected
の順。
test_weird_simple_case
は引数がexpected
、n
の順です。
@pytest.mark.parametrize
はパラメタの名を見ていい感じに処理してるってことなんでしょうか?
仕組みが気になる〜!
💡2️⃣ デコレータを複数積める(自動で組合せてくれる!)
@pytest.mark.parametrize
は、なんと複数積めるんです!
これによりpytestがパラメタの値を全通り組合せてくれます!
ドキュメントの例です。
import pytest @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): print(f"{x=}", f"{y=}") # ドキュメントではpass
collected 4 items test_expectation.py x=0 y=2 .x=1 y=2 .x=0 y=3 .x=1 y=3 .
(x,y)が(0,2),(1,2),(0,3),(1,3)と全通り組み合わさっていますよね!
人が書く必要がなくなってとっても便利!大助かりです。
これが非常にありがたかったのは、引数が多い関数のテストを書くときです。
多くの引数を取ってbool値を返す関数がありました6。
説明用の例としてdef f(a, b, c) -> bool:
としましょう。
振る舞いは申し分ないですが、将来の開発者のことを考えるとあまりいい構造ではありません。
そこでリファクタリングをするというシーンです。
この関数にはテストがありませんでした。
なので、リファクタリング中に誤りに気付けるようにまずテストを書きます。
引数を同値分割していくと2のN乗のパターンがありました。
ここでa
がこれこれのときはb
やc
の値にはよらずに関数f
の返り値が常にFalse
というのを発見しました7。
これをパラメタ化されたテストで表すにあたって、上の例のxとyをbとcに置き換える形で、test_aがこれこれのときはFalseを返す
を書いています。
「bとcの組を全部人が書くのか...」と怠惰な私の心が拒否反応を示したのですが、pytest
のparametrize
で自動化できることを知り、開発が大変はかどりました!
ノードIDについて
なお、pytest -v
で確認できるノードIDは、内側の(=下に配置された)parametrizeの引数が先に表示されます。
ドキュメントの例だと[y-x]
です。
collected 4 items test_expectation.py::test_foo[2-0] PASSED [ 25%] test_expectation.py::test_foo[2-1] PASSED [ 50%] test_expectation.py::test_foo[3-0] PASSED [ 75%] test_expectation.py::test_foo[3-1] PASSED [100%]
parametrizeデコレータの順を入れ替えると、ノードIDは[x-y]
となります。
@pytest.mark.parametrize("y", [2, 3]) # yが上になった @pytest.mark.parametrize("x", [0, 1]) def test_foo(x, y): print(f"{x=}", f"{y=}")
collected 4 items test_expectation.py::test_foo[0-2] PASSED [ 25%] test_expectation.py::test_foo[0-3] PASSED [ 50%] test_expectation.py::test_foo[1-2] PASSED [ 75%] test_expectation.py::test_foo[1-3] PASSED [100%]
ドキュメント「How to parametrize fixtures and test functions」にはまだまだtips満載!
メモとして残します(『テスト駆動Python』での学びも含みます)
@pytest.mark.parametrize
のids
引数でノードIDを指定できる- ノードIDで非アスキー文字はデフォルトでエスケープされる
- ドキュメントのNoteによるとエスケープの無効化設定もあるが、望まない副作用といったリスクもあるそうで、利用は自己責任で
@pytest.mark.parametrize
にシーケンスで渡すパラメタの1個1個はpytest.param
にできるpytest.param
にmarksやidを指定できる- idはノードIDの指定
- パラメタ化されたテストの1つ1つのパラメタについて
pytest.param
できめ細かく設定できるという理解(素振りしたい)
- 例から学べるドキュメント(積ん読)
終わりに
パラメタ化されたテストに使う@pytest.mark.parametrize
について知ったtips 2つをアウトプットしました。
- クラスもデコレートできる
- 複数積んで、自動でパラメタを組合せられる
直近では2に助けられました〜。
また1を実現する仕組みが気になるところです。
ドキュメントを見ていてもpytestにはまだまだ私が知らない機能がある感じで、ちょっとずつ知っている範囲を増やしていければと思っています。
P.S. 積めるのに気づけたのはChatGPTのおかげ!
boolを返す関数のテストをpytestで書いていて、Trueを返すパラメタの組合せをparametrizeに全部列挙するのツライな、もっといい方法ないかなと #ChatGPT に雑に聞いたら、stackできると教えてくれました!
— nikkie にっきー (@ftnext) 2023年4月12日
パラメタの組合せはpytestにお任せ!https://t.co/EuBXJVBp4J#SIGNATE#テーマ_AIペアプロ pic.twitter.com/tuY0nvagHP
からあげさんのブログでSIGNATEさんの活用例コンペを知っていたので、「AIとのペアプロ成功例と言えるのでは?」と投稿してみました(上のツイートをリツイートして応援してくれたら嬉しいです)
からあげさん、頑張ってください!
(リツイート数で争うの、ちょっと『パリピ孔明』みがありますね。EIKO-san v.s. AZALEA)
- そういえば第2版が出たそうです O'Reilly Japan - 退屈なことはPythonにやらせよう 第2版↩
- 監修者のやっとむさんによるトークのレポートがこちら:みんなのPython勉強会#88のやっとむさんによる「手軽なpytestでテストを活用しよう!」、テストコードに関係する知識が結び付き、刺激的でした #stapy - nikkie-ftnextの日記↩
-
unittestモジュールに経験がある向きにお伝えすると、ここで紹介する
@pytest.mark.parametrize
は、厳密には動きに差があるのですが、subTestのようなものです↩ -
↩pytestでもクラスの入れ子で表現できるらしい。知らなかった。(YouTubeアーカイブのコメント見ていて気が付いた)#udonpyhttps://t.co/OtYLkljiPi
— Hiroshi Sano (@hrs_sano645) 2021年8月27日 -
print
デバッグ+pytest -s
で確認してもn
には1と3が渡っています↩ - 『Clean Code』3章にある「さまざまな引数の組み合わせを網羅するテストケースを書く難しさ(Kindle の位置No.1424-1425)」を体感しました。組合せが爆発してしまっていて、すべての組合せを網羅するのはマジ困難!(ここで紹介したpytestのtipsがなかったら絶望していたでしょう)↩
- テストを先に書いていたら、そもそも多くの引数を持たなかった、なので、同値分割してテストを書くのも不要だったのではないかと思われます(一度一緒になったものを引き剥がすのはこんなにも大変ということですね)↩