pytest が柔軟すぎてなかなか扱いが難しい(ときがある)という話

個人的には RSpec の手法が好きで緩い BDD で書くことが習慣化している。

具体的にはテスト対象でディレクトリを切って文脈ごとにスコープを切るという RSpec を普通に使う場合の方法だ。property based testing や generative testing や golden data testing なども適用できるなら挑戦してみてはいるが、一番しっくり来ているやりかたが先述のものというだけで都度考えてはいる。

で、私は python を公私ともによく使っていて一番使い慣れているわけだけども python の標準テストパッケージはかなり優れていると思っているので特に不満がないが pytest も素晴らしい。

pytest 自体の素晴らしさは柔軟さと手軽さに起因していると思うが、それゆえ使いようによっては一貫性を簡単に崩すこともできる。

自分にとってのベストプラクティスを押し付けることは悲しみしか生まないことがあるので教訓としていろいろと最近思ったことを記す。

テストケース内で分岐するパターン

この記事で一番強調したいトピックがこれ。 pytest の便利機能で代表的なものとして parametrize が挙げられる。これは基本的にテストデータを生成するためであってテストパターンを生成すべきではないと考えているがそうでもない使われ方にあたったことがある。(是非の話ではない。)

@pytest.mark.parametrize(['flg1,flg2', ((True, False), (True, True), (False, False))])
def test_hoge(flg1, flg2):
    if flg1 and flg2:
        assert nantoka
    elif flg1:
        assert kantoka
    else:
        assert nantara

といった具合だ。たしかにテストケースを宣言する手間が省けるかもしれないがこれはテストコードを読んでさらに条件を判定しなくてはならず、何をテストしたいかが一瞬で把握しづらいというデメリットがある。 また、特定の条件にだけ必要なセットアップなどもでてくるといよいよテストコードが意味としても記述としても複雑になる。 特にコンテキストマネージャーが出てくると冥府魔道への扉が開かれ悲惨さを体現するばかりである。

from context import ExitStack

# どうだろう。複雑怪奇ではないか。
@pytest.mark.parametrize(...)
def test_1(flg1, flg2, flg3):
    with ctx1, ctx2, ExitStack() as stack:
        if flg1 and flg3:
            stack.enter_context(special1_3)
        if flg2 and flg3:
            stack.enter_context(special2_3)
        ...

切り替えのフラグが少なければ効果的かもしれないがいくつもフラグで分かち書きが出てくるようなら素直にテストケースを分けるのが筋だろう。

@pytest.fixture
def nantoka_setup():
    ...
    yield

def test_expect_nantoka(nantoka_setup):
    ...

@pytest.fixture
def kantoka_setup():
    ...
    yield

def test_expect_kantoka(kantoka_setup):
    ...

この手間を惜しむには代償がでかい場合がある。

そもそも、シナリオを記述するインテグレーションテストに例外はあるとしてテストコードは宣言的に書かれるべきであり手続き的に書くのは明確に誤っていると断言してもいい。 ただし、テストコードの抜本的なリファクタリングは誰もやりたがらず誤ったルールで一貫性を保とうとする善意の結果、未来永劫割れ窓理論が適用され続けるので直そうとする場合はそれ相応に強い意志が試される。

クラスベースにするか関数ベースにするか

これはもう好きにすればいいと思うし関数ベースじゃないとできないみたいな局面はない。 が、pytest を使う場合においてはクラスで表現される self が fixture で代替できるため余計な引数を排除するという意味では関数ベースをまず最初に選択したい。

ただ、クラスを名前空間として用いることがテストの書き易さに直結することもあるのでやはり良しあしだろう。一貫性はどちらが保ちやすいかなどはチーム内で相談して指針を出せばいいと思う。(というかテストの書き方というともっと本質的な部分があるし…)

少々一般的な話になるが self がないぶん関数デコレーターがメソッドデコレーターよりも素直に使えるという微小の利点もある。

# 別にどっちが優れてるとかない

class T:
    @pytest.fixutre(autouse=True)
    def init(self):
       # fixture ベースでデータを扱うなら self が全く不要ってのはある
        ...

   def test_1(self):
        ...


# vs 


@pytest.fixture(autouse=True)
def init():
    ...


def test_1():
    ...

データファクトリとかフィクスチャの定義場所

これもチーム内で合意がとれていればいいと思うし別に pytest に限った話では全くない。 ただし、pytest の fixture でおおむね間に合うので無理に factory_boy を導入する必要はなかったというのを体験したことがある。とはいえ Fuzzer は便利なのだが。

大事なことはスコープを意識することだと思う。あらゆるテストに必要な前後処理やファクトリ関数はテストディレクトリのルートに定義して特定のテスト対象にしか使わないものはスコープ内に収めるようにするというアプリケーションコードでも変わらんルールを適用すれば大きなケガや後悔をせずにすむのではなかろうか。とにかく、協調性を以て道具をうまく使うことが大事なのである。