pytest フィクスチャの実行タイミングと返却値の参照方法のまとめ (scope, autouse 指定時)

scope, autouse 両指定時の返却値の参照方法とそれに実行タイミングが影響を受けないかについて説明やサンプルスクリプトを書ききっている記事が見受けられなかったので、以下の「まとめ」にまとめ、サンプルスクリプトを書きました (仕様は pytest-8.3.4 現在です)。[^1]

[^1]: 最近久しぶりに pytest を書いたのですが、なぜかフィクスチャを `autouse=True` にしておけばテスト関数の引数に書かなくても返却値を参照できると思っていました。しかし、返却値を参照するならば明示的に引数に書く必要がありました。なお、引数に書かない場合は返却値ではなくフィクスチャ関数自体が参照され、そういわれるとそれはそうなるだろうと思います。ただ、なぜか参照できると思い込んでおり、Web 上にもあたかも参照できるような日本語記事があるので (ただ完全なスクリプトはない)、参照できた時期もあったのか存在しない記憶であったのかわかりません。

::: note
実行環境は Python 3.11.4, pytest-8.3.4 です。
::: 

### まとめ

以下の 1. の記述は参考文献 1., 2. および手元での確認結果に基づいています。2. の記述については、ドキュメントには `autouse=True` フィクスチャの返却値の参照例がないため比較的手元での確認結果ベースです (i.e. 公式の想定する使用方法なのかは保証できません)。

1. pytest のフィクスチャは原則として、<INS>テスト関数から引数で要求されたとき (あるいは要求された他フィクスチャから引数で要求されたとき)、その都度実行される</INS>[^2]。ただし、
    1. `autouse=True` が指定された場合は、フィクスチャ視点、<INS>「そのフィクスチャの影響下にあるすべてのテスト関数」がそのフィクスチャを要求しているものとみなす <b>(※)</b></INS>。
    1. デフォルトの `scope='function'`より広い `scope='session'` のようなスコープが指定された場合は、<INS>スコープ内での最初の要求時に初めて実行され、2回目以降の要求時は再実行されずに実行済み結果が再利用され、もし後処理があれば (= yield フィクスチャであれば) スコープ内のすべてのテスト関数終了後に実行される</INS>。[^3]
1. pytest のフィクスチャの return (yield) 値をテスト関数側から実際に参照したい場合、<INS>常に引数に明記する必要がある</INS> (フィクスチャ側に `autouse=True` を指定していたとしても、テスト関数視点では明示的に要求しなければならない；<INS>この明示的要求によってスコープ付きフィクスチャが余計に再実行されるようなことはない</INS>)。

[^2]: フィクスチャ hoge をテスト関数の引数に指定して、そのテスト関数内で何度も hoge を参照する分にはその度に再実行はされません。参考文献 1. の <b>Fixtures can be requested more than once per test (return values are cached) </b>という見出しを参照してください。
[^3]: ただし、前処理内でエラーがあった場合 (Ex. 環境変数がまだ立っていないかを assert するべきなのに、誤ってもう立っているかを assert するなどすれば失敗し続けます)、セッションスコープを指定したのにテスト関数ごとに実行がチャレンジされ続けるはずです (実行済み結果がずっと存在しないのでこの動きになると思います)。このエラーをみると、あたかもスコープが壊れているのかと思えますが、壊れているのは自分が記述した判定なので注意が必要です。

<b>(※)</b> ここで、「そのフィクスチャの影響下にあるすべてのテスト関数」とは、そのフィクスチャが `conftest.py` に定義されている場合はそのディレクトリ階層以下にあるすべての `test_*.py` 内のテスト関数、そのフィクスチャが `test_x.py` のトップレベルに定義されている場合はそのファイル内のテスト関数、そのフィクスチャが `class TestX:` インスタンスメソッドとして定義されている場合はそのクラス内のテスト関数といった具合です。

### 参考文献

1. [How to use fixtures - pytest documentation](https://docs.pytest.org/en/stable/how-to/fixtures.html)： `autouse=True` にしたときの返却値を参照するような例はありませんでした。
1. [Fixtures reference - pytest documentation#autouse-fixtures-are-executed-first-within-their-scope](https://docs.pytest.org/en/stable/reference/fixtures.html#autouse-fixtures-are-executed-first-within-their-scope)： `autouse=True` にしたときの実行順があります。冒頭の説明では説明を複雑にしないために省きましたが、「そのフィクスチャが要求される時」とは、テスト関数から直接要求される時だけでなく、他のフィクスチャから間接的に要求される時も含みます。

### サンプルスクリプト

例えば全テスト実行前に環境変数を立てて、全テスト実行後に環境変数を削除したいとし、かつ、環境変数に立てた値を各テストから必要に応じて受け取りたいとします。このようなとき、以下の `my_key` のようなフィクスチャを使用すると思います。

```py:test_0.py
# ----- ここから下は本来 conftest.py に分離する -----
# セッションスコープのフィクスチャは本来 conftest.py に書くべきである。
# さもないと、テストファイルが複数あったとき当該テストファイルのテスト実行時まで実行されない。
# つまり、scope='session' とあるのに実際にはそうならない。
# そのような、コードを名が体を表していない状態にすることは避けるべきである。

import pytest
import os

@pytest.fixture(scope='session', autouse=True)
def my_key():
    print('\n===== setup =====')
    assert os.environ.get('MY_KEY') is None
    os.environ['MY_KEY'] = 'hoge'
    yield os.environ['MY_KEY']
    print('\n===== teardown =====')
    os.environ.pop('MY_KEY')

# ----- ここから上は本来 conftest.py に分離する -----

def test_00():
    print('\n----- test_00 -----')

def test_01(my_key):  # 受け取りたいときは引数に明記する
    print('\n----- test_01 -----')
    assert my_key == 'hoge'

def test_02(my_key):  # 受け取りたいときは引数に明記する
    print('\n----- test_02 -----')
    assert my_key == 'hoge'

def test_03():
    print('\n----- test_03 -----')
```

`pytest -s` の実行結果は以下になります。

```
test_0.py
===== setup =====

----- test_00 -----
.
----- test_01 -----
.
----- test_02 -----
.
----- test_03 -----
.
===== teardown =====
```

フィクスチャに `autouse=True` を付けない場合、setup タイミングが最初の受け取り要求時にずれます。teardown タイミングが全テスト終了後であるのは変わりません。
```
test_0.py
----- test_00 -----
.
===== setup =====

----- test_01 -----
.
----- test_02 -----
.
----- test_03 -----
.
===== teardown =====
```

なお、`scope=function` にすると `my_key` の setup と teardown が各テストの前後で実行されるようになります。さらに `autouse=True` を外すと `my_key` を要求するテストの前後でのみ実行されるようになります (スクリプト略)。

また、上記のスクリプトとほぼ同じでフィクスチャ名だけ `my_key_1` にリネームしたスクリプト `test_1.py` を追加すると、`my_key_1` は `test_1.py` のテストの順番が回ってくるまで実行されません (スクリプト略)。これではセッションスコープになっておらず、`conftest.py` に切り分けるべきです (切り分けると解決します) (スクリプト略)。