pytestのfixtureの実行順序を調べてみた

2019.12.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部の夏目です。

案件ではPythonを使ってLambdaを実装しているのですが、テストにpytestを使用しています。

pytestにはsetup/teadownの機能としてfixtureなるものがあるのですが、使用すると記載する場所で実行順序が異なるので調べてみました。

pytestのfixture

fixutureとはテスト関数の実行前後に実行される関数です。
テストデータを用意したりすることに使えます。

テストファイル内で定義することもできるし、conftest.pyというファイルで定義すれば複数のテストファイルで使用することができる。

調べてみた

概要

  • conftest.pyので定義したfixtureと、テストファイルで定義したfixtureを使う
  • 各fixtureで同一ファイルに追記を行うことで、順番を特定する
  • スコープは全てfunctionとする (シンプルにするため)
  • テストに対して使用するfixtureを設定する箇所は次のものを想定
    • conftest.pyに定義したautouseなfixture
    • テストファイルに定義したautouseなfixture
    • テストクラスの@pytest.mark.usefixture()で指定したfixture
    • テスト関数の@pytest.mark.usefixture()で指定したfixture
    • テスト関数の引数で指定したfixture

コード

conftest.py

import pytest


def add_message(msg: str):
    open(f'result.txt', mode='a').write(f'{msg}\n')


@pytest.fixture(scope='function', autouse=True)
def fixture_auto_use_conftest():
    mes = 'auto_use_conftest'
    add_message(f'start {mes}')
    yield # fixtureはyieldで区切ると、yieldより前の処理をテストの前に、yieldより後の処理をテストの後に、実行する
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_class_usefixtures():
    mes = 'class_use_fixture'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_class_indirect_and_usefixtures(request):
    mes = 'class_indirect_and_usefixtures'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_class_indirect(request):
    mes = 'class_indirect'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_func_indirect_and_usefixtures(request):
    mes = 'func_indirect_and_usefixtures'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_func_indirect(request):
    mes = 'func_indirect'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_func_usefixtures():
    mes = 'func_usefixtures'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_func_call_01():
    mes = 'func_call_01'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.fixture(scope='function')
def fixture_func_call_02():
    mes = 'func_call_02'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')

test_main.py

import pytest
from conftest import add_message


@pytest.fixture(scope='function', autouse=True)
def fixture_auto_use_test_file():
    mes = 'autouse_test_file'
    add_message(f'start {mes}')
    yield
    add_message(f'end {mes}')


@pytest.mark.parametrize(
    'fixture_class_indirect_and_usefixtures, fixture_class_indirect', [
        (None, None)
    ], indirect=True
)
@pytest.mark.usefixtures('fixture_class_usefixtures', 'fixture_class_indirect_and_usefixtures')
class TestMain(object):
    @pytest.mark.parametrize(
        'fixture_func_indirect_and_usefixtures, fixture_func_indirect', [
            (None, None)
        ], indirect=True
    )
    @pytest.mark.usefixtures(
        'fixture_func_usefixtures',
        'fixture_func_indirect_and_usefixtures'
    )
    def test_pass(self,
                  fixture_func_call_01,
                  fixture_class_indirect,
                  fixture_func_indirect,
                  fixture_func_call_02):
        add_message('====')

実行結果

result.test

start auto_use_conftest
start autouse_test_file
start func_usefixtures
start func_indirect_and_usefixtures
start class_use_fixture
start class_indirect_and_usefixtures
start func_call_01
start class_indirect
start func_indirect
start func_call_02
====
end func_call_02
end func_indirect
end class_indirect
end func_call_01
end class_indirect_and_usefixtures
end class_use_fixture
end func_indirect_and_usefixtures
end func_usefixtures
end autouse_test_file
end auto_use_conftest

結論

parameterizeのindirectなども考えたけども結論はこれ。

注意が必要そうなのは、@pytest.mark.usefixture()で指定したfixtureにおいて、関数で指定したfixtureの後にクラスで指定したfixtureが動くこと。

まとめ

pytestのfixtureの実行順序についてまとめてみました。

ここまで面倒な使い方をすることはないと思いますが、こういう順序で実行されるんだ程度に覚えておくと良さそうです。