8
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

pytestのテスト関数にパラメータつけてみる(parametrize, pytest_generate_tests)

単体テスト書いてますか?

めんどくさくてサボりがちになってしまいますが。ていうか、サボってますが。今後もコードを触る可能性があるなら、単体テストを書く作業は必ずペイします。低リスク、利益率数百パーセントという夢のような投資です。

これ読みに来てる人は、そんなの分かってると信じて。
テストコードをコピペする作業に飽きた人のために、pytestのテスト関数にパラメータ付けをしてみます。

今回は、私が開発している量子コンピュータ用ライブラリBlueqatで実際に使われているテストコードから、実際の例を抜き出してみます。

pytest.mark.parametrizeを使う

見た方が早いので、いきなり実例です。

parametrizeを使う
@pytest.mark.parametrize('arg, expect', [
    ("01011", (0, 1, 0, 1, 1)),
    ({"00011": 2, "10100": 3}, {(0, 0, 0, 1, 1): 2, (1, 0, 1, 0, 0): 3}),
    (Counter({"00011": 2, "10100": 3}), Counter({(0, 0, 0, 1, 1): 2, (1, 0, 1, 0, 0): 3}))
])
def test_to_inttuple(arg, expect):
    assert to_inttuple(arg) == expect

parametrizeを使わなければ、恐らくこうなったでしょう。

parametrizeを使わなかった場合
def test_to_inttuple1():
    assert to_inttuple("01011") == (0, 1, 0, 1, 1)

def test_to_inttuple2():
    assert to_inttuple({"00011": 2, "10100": 3}) == {(0, 0, 0, 1, 1): 2, (1, 0, 1, 0, 0): 3}

def test_to_inttuple3():
    assert to_inttuple(Counter({"00011": 2, "10100": 3})) == Counter({(0, 0, 0, 1, 1): 2, (1, 0, 1, 0, 0): 3})

テスト関数は、なにもなければ引数なしで作りますが、既に見たように

parametrizeの使い方
@pytest.mark.parametrize('引数1, 引数2, ...', [
    (テストケース1の引数1, テストケース1の引数2, ...),
    (テストケース2の引数1, テストケース2の引数2, ...),
    ...
])
def test_hogehoge(引数1, 引数2):
    assert hogehoge(引数1, 引数2)

のようにできます。引数が1つの場合、

parametrizeの使い方(1引数)
@pytest.mark.parametrize('引数', [テストケース1の引数, テストケース2の引数, ...])
def test_hogehoge(引数):
    assert hogehoge(引数)

でも構いません。

便利ですね。

テストケースをコマンドラインオプションに応じて、動的に作る

parametrizeで十分便利なんですが。最近、開発しているBlueqatに、NVIDIA GPUでしか動かないオプション機能が追加されました。
CIツールとしてCircle CIを使っていますが、GPUのコードは動かないので、そっちは自分の開発環境でテストして、Circle CIではGPUなしで動くコードを動かしたいという需要ができました。(ほんとはGPUもCircle CIでやりたいが。GPUインスタンス立てる金ないから許して)

また、同時期に類似のオプション機能の追加があったので、それも含めてテストしたい、となりました。

Blueqatでは、以下のようにして、裏で使うプログラム(backendと呼んでいます)を選べます。

backendの指定
c = Circuit()
c.run() # => デフォルトのbackendで動く
c.run(backend='numpy') # => 'numpy'という名前のbackendで動く

今まで、デフォルトの'numpy' backendしかテストしてなかったのですが、新たに'numba' backendとGPU必須の'qgate' backendが加わりました。
テストコードの再利用と、テストするbackendを選ぶ(Circle CIではGPU必須のコードを外す)需要が出たので、コマンドラインオプションからテストするbackendを選べるようにしました。

pytest_addoptionでコマンドラインオプションを追加する

テストのあるディレクトリにconftest.pyというファイルを作り、そこにpytest_addoptionという関数を定義し、その中でparser.addoptionを呼んでオプションを追加します。

conftest.py(コマンドラインオプションの追加)
def pytest_addoption(parser):
    parser.addoption('--add-backend', default=['numpy'], action='append')

parser.addoptionの引数は、ドキュメントには、標準ライブラリのargparse.add_optionと同じだと書いてありますが、argparse.add_argumentだと思われます。

上に書いたコードでは、--add-backendオプションを定義し、デフォルト値を['numpy']としています。--add-backend numbaとコマンドラインオプションを指定すると、その値が追加(append)され['numpy', 'numba']となります。

pytest_generate_testsでパラメータ化する

これもconftest.pyに書きます。

conftest.py(コマンドラインオプションからテストを作る)
def pytest_generate_tests(metafunc):
    if 'backend' in metafunc.fixturenames:
        metafunc.parametrize('backend', metafunc.config.getoption('--add-backend'))

テストの引数にbackendが含まれていたら、backendに、--add-backendオプションで指定されたリストでparametrizeします。

続いて、他のテストファイルで、backendパラメータを使ったテストを書きます。

test_*.py
def test_hgate1(backend):
    assert is_vec_same(Circuit().h[1].h[0].run(backend=backend), np.array([0.5, 0.5, 0.5, 0.5]))


def test_hgate2(backend):
    assert is_vec_same(Circuit().x[0].h[0].run(backend=backend),
                       np.array([1 / np.sqrt(2), -1 / np.sqrt(2)]))

これらは
python -m pytest .
のように動かした場合は

test_*.py(pytest_generate_testsを使わずparametrizeで書く・その1)
@pytest.mark.parametrize('backend', ['numpy'])
def test_hgate1(backend):
    assert is_vec_same(Circuit().h[1].h[0].run(backend=backend), np.array([0.5, 0.5, 0.5, 0.5]))


@pytest.mark.parametrize('backend', ['numpy'])
def test_hgate2(backend):
    assert is_vec_same(Circuit().x[0].h[0].run(backend=backend),
                       np.array([1 / np.sqrt(2), -1 / np.sqrt(2)]))

と同じになります。また、
python -m pytest . --add-backend numba --add-backend qgate
のように動かした場合は

test_*.py(pytest_generate_testsを使わずparametrizeで書く・その2)
@pytest.mark.parametrize('backend', ['numpy', 'numba', 'qgate'])
def test_hgate1(backend):
    assert is_vec_same(Circuit().h[1].h[0].run(backend=backend), np.array([0.5, 0.5, 0.5, 0.5]))


@pytest.mark.parametrize('backend', ['numpy', 'numba', 'qgate'])
def test_hgate2(backend):
    assert is_vec_same(Circuit().x[0].h[0].run(backend=backend),
                       np.array([1 / np.sqrt(2), -1 / np.sqrt(2)]))

と同じになります。

ややハードルが上がりますが、一度がんばってしまえば楽で、メリットは大きいかと思います。

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
8
Help us understand the problem. What are the problem?