pytestのparametrizeと仲良くなりたくて

はじめに

何事もまずは標準装備の機能からちゃんと使えるようになろうと思って、PythonのUnittestをちょくちょく触っていたんですが、案件ではpytestを使っています。pytestの書き方にも慣れてきて、毎日読んだり書いたりしていますが、受け身一方で身の回りにあるコード例しか知らない。これはあんまり良くないと思って、ちゃんと自力でディグって使えそうなコマンドとか機能を発掘しようというと思い立ちました。あと、pytest.Parameterizeと仲良くなりたくて。Parameterizeが本題です。

成果物

pytest.Parametarize の基本

ここに関数があります。StringをIntに変換してくれるだけのやつです。世界一作る意味の無い関数の内の1つですね。

def convert_str_into_int(x):
    return int(x)

さて、この関数をテストするコードを愚直に書いてみます。

@pytest.fixture
def target():
    return convert_str_into_int

def test_convert_str_into_int(target):
    expected = 10
    actual = target("10")
    assert actual == expected

そして pytest で実行だっ!

$ pytest
============================= test session starts ==============================
platform darwin -- Python 3.7.1, pytest-4.1.1, py-1.7.0, pluggy-0.8.1
rootdir: /Users/kai/Github/parametrize, inifile:
collected 1 item

test_parametrizing.py .                                                  [100%]

=========================== 1 passed in 0.01 seconds ===========================

ちゃんとPassedになりました。

parametrizingする

ただし、10でテストが通ったからって100で通るとは限らない。0ならどうだろう。-100なら。999999999999とか、いろんなケースでテストしたいときがあります。そんなときにparametrizeが使えるんですね。いま列挙したケースをparametrizeを使って書くと以下の通りになります。

@pytest.mark.parametrize("inputs, expected", [
    ("10", 10),
    ("100", 100),
    ("0", 0),
    ("-1", -1),
    ("999999999", 999999999)])
def test_convert_str_into_int(inputs, expected, target):
    actual = target(inputs)
    assert actual == expected

実行結果はちゃんとPassedしています。

$ pytest
============================= test session starts ==============================
platform darwin -- Python 3.7.1, pytest-4.1.1, py-1.7.0, pluggy-0.8.1
rootdir: /Users/kai/Github/parametrize, inifile:
collected 5 items

test_parametrizing.py .....                                              [100%]

=========================== 5 passed in 0.04 seconds ===========================

テストが通るってしあわせですよね。

パラメータにidをつける

あんまり長ったらしいパラメータをデコレーション内に書くとコードが読みづらくなるので、リファクタリングします。ついでに、パラメータごとにidを設定してみましょう。以下のようにパラメータとidを準備します。

test_data = [
    ("10", 10), 
    ("100", 100),
    ("0", 0),
    ("-1", -1),
    ("999999999", 999999999)
]


test_ids = [
   "10",
   "100",
   "0",
   "-1",
   "99999999",
]

そしてpytestのデコレーション側で上で定義した test_datatest_ids を指定します。

@pytest.mark.parametrize("inputs, expected", test_data, ids=test_ids)
def test_convert_str_into_int(target, inputs, expected):
    actual = target(inputs)
    assert actual == expected

idはテストが失敗した時にどのパラメータで失敗したかをすぐに識別するのに役に立ちます。また、pytest --collect-only を使うと有効なテストのみをpytestが収集して表示してくれるのでidを確認することができます。pytest --collect-only をしてみます。

============================= test session starts =============================
platform darwin -- Python 3.7.1, pytest-4.1.1, py-1.7.0, pluggy-0.8.1
rootdir: /Users/kai/Github/parametrize, inifile:
collected 5 items
<Module test_parametrizing.py>
  <Function test_convert_str_into_int[10]>
  <Function test_convert_str_into_int[100]>
  <Function test_convert_str_into_int[0]>
  <Function test_convert_str_into_int[-1]>
  <Function test_convert_str_into_int[99999999]>

========================= no tests ran in 0.01 seconds ==========================

ちゃんと [] の中にidが表示されています。

Fixtureとパラメータを一緒に使う

Fixtureを使うテストにもparametrizeで値を与えることができます。またparametizeで設定した値を、Fixture内で処理してからテストしたい時があります。そういうときは引数にダイレクトに値を与えるのではなく、Fixtureを通して引数として設定するためのオプションindirectを使います。

@pytest.fixture(scope='function')
def greet(request):
    if request.param == 'dog':
        return 'bow-bow'
    if request.param == 'cat':
        return 'meow'
    return '... ...'

params = [
            ('dog', 'bow-bow'), 
            ('cat', 'meow'), 
            ('cow', '... ...')
        ]

@pytest.mark.parametrize('greet, expected', params, indirect=['greet'])
def test_indirect(greet, expected):
    assert greet == expected

indirect はキーワード引数で指定したもののみに適応されます。今回はgreetフィクスチャに適応されています。もし、indirectがなければ、greetにはdog, cat, cowがそのまま引数として渡されてしまいます。

parametrizeはこれで終わり。


pytestの小ネタ

なんか面白いコマンドや機能はないかなと、調べてみました。

Skipの理由を書く

たまにテストをSkipすることがある。@pytest.mark.skipでスキップすることができる。どうせならSkipの理由を表示すると良いんじゃないかなぁと思う。次のように書ける。

@pytest.mark.skip(reason="Skip this test for test")

実行してみる。実行コマンドに-rsをつけることを忘れちゃいけない。

$ pytest -rs
============================= test session starts ==============================
platform darwin -- Python 3.7.1, pytest-4.1.1, py-1.7.0, pluggy-0.8.1
rootdir: /Users/kai/Github/parametrize, inifile:
collected 3 items

test_parametrizing.py sss                                                [100%]
=========================== short test summary info ============================
SKIP [3] test_parametrizing.py:30: Skip this test for test

========================== 3 skipped in 0.02 seconds ===========================

条件に応じてSkipする

これはいまいち使い所がわからないが、いつか使うかもしれない。

@pytest.mark.skipif(
  os.environ['ENV'] == 'dev',
  reason="Skip this test for test"
  )

このようにスキップする条件を書くことができる。

タグ付けしてみる

テストに好きな名前のタグをつけることができる。

@pytest.mark.hoge

hogeというタグをつけてみた。hogeというタグのついたテストだけを実行したいなら以下のコマンドで実行できる。

pytest -m hoge

逆にhogeのタグがついたテストを実行したくないときは次のように実行すればよい。

pytest -m "not hoge"

コマンドラインオプションを実行する

おわりに

docs.pytest.org

docs.pytest.org

blog.thedigitalcatonline.com