単体テスト書いてますか?
めんどくさくてサボりがちになってしまいますが。ていうか、サボってますが。今後もコードを触る可能性があるなら、単体テストを書く作業は必ずペイします。低リスク、利益率数百パーセントという夢のような投資です。
これ読みに来てる人は、そんなの分かってると信じて。
テストコードをコピペする作業に飽きた人のために、pytestのテスト関数にパラメータ付けをしてみます。
今回は、私が開発している量子コンピュータ用ライブラリBlueqatで実際に使われているテストコードから、実際の例を抜き出してみます。
pytest.mark.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
を使わなければ、恐らくこうなったでしょう。
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})
テスト関数は、なにもなければ引数なしで作りますが、既に見たように
@pytest.mark.parametrize('引数1, 引数2, ...', [
(テストケース1の引数1, テストケース1の引数2, ...),
(テストケース2の引数1, テストケース2の引数2, ...),
...
])
def test_hogehoge(引数1, 引数2):
assert hogehoge(引数1, 引数2)
のようにできます。引数が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と呼んでいます)を選べます。
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
を呼んでオプションを追加します。
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
に書きます。
def pytest_generate_tests(metafunc):
if 'backend' in metafunc.fixturenames:
metafunc.parametrize('backend', metafunc.config.getoption('--add-backend'))
テストの引数にbackend
が含まれていたら、backend
に、--add-backend
オプションで指定されたリストでparametrizeします。
続いて、他のテストファイルで、backendパラメータを使ったテストを書きます。
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 .
のように動かした場合は
@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
のように動かした場合は
@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)]))
と同じになります。
ややハードルが上がりますが、一度がんばってしまえば楽で、メリットは大きいかと思います。
参考文献