pytestのfixtureのスコープとは

2024/11/23に公開

fixtureのスコープとは

fixtureでは セットアップとティアダウンを定義することができます。
スコープはそのセットアップとティアダウンの実行するタイミングを決定する役割を持ちます。

functionスコープ

説明するよりも、実際にコードを見てみましょう。test_sample.pyというファイル名で以下のコードを作成します。

import pytest

@pytest.fixture()
def db():
    db = []
    yield db
    del db

def test_empty(db):
    assert len(db) == 0

def test_non_empty(db):
    db.append("dog")
    db.append("cat")
    assert len(db) == 2

def test_non_empty_again(db):
    db.append("dog")
    assert len(db) == 1

このコードでは、リストに値を保持するfixtureをdbという名前で定義しています。
dbは空のリストで初期化(セットアップ)され、テストが終わったらリストを削除(ティアダウン)しています。この処理は個々の関数ごとに行われるため、当たり前ですがこのテストは成功します。

ここで、--setup-showオプションを使ってテストのセットアップとティアダウンの順序を確認してみましょう。

$ pytest --setup-show test_sample.py
test_sample.py 
        SETUP    F db
        pytest_fixture_scope/test_sample.py::test_empty (fixtures used: db).
        TEARDOWN F db
        SETUP    F db
        pytest_fixture_scope/test_sample.py::test_non_empty (fixtures used: db).
        TEARDOWN F db
        SETUP    F db
        pytest_fixture_scope/test_sample.py::test_non_empty_again (fixtures used: db).
        TEARDOWN F db

pytest_fixture_scope/test_sample.py::test_XXX (fixtures used: db).の前後にSETUPTEARDOWNが実行されているのがわかります。
また、F dbというのはdbというfixtureがfunctionスコープで実行(関数ごとに実行)されていることを示しています。

なので、先ほどのコードは、fixtureの定義を@pytest.fixture(scope="function")としていることと同じです。

moduleスコープ

次に、dbのスコープをmoduleに変更してみましょう。

@pytest.fixture(scope="module")  # ここを変更
def db():
    db = []
    yield db
    del db

def test_empty(db):
    assert len(db) == 0

def test_non_empty(db):
    db.append("dog")
    db.append("cat")
    assert len(db) == 2

def test_non_empty_again(db):
    db.append("dog")
    assert len(db) == 1

すると先ほどのテストは失敗します。
moduleスコープにしたことで、dbのセットアップとティアダウンはモジュール(.pyファイル)ごとに1回だけ実行されるようになり、test_non_empty_againの実行時にはtest_non_emptyで追加したにはdogとcatがdbに残っているため、len(db) == 1が失敗します。

--setup-showオプションを使ってテストのセットアップとティアダウンの順序を確認すると、確かにdbのセットアップとティアダウンがモジュールごとに1回だけ実行されていることがわかります。また、SETUP M dbのようにFがM(module)に変わっているのがわかります。

test_sample.py 
    SETUP    M db
        pytest_fixture_scope/test_sample.py::test_empty (fixtures used: db).
        pytest_fixture_scope/test_sample.py::test_non_empty (fixtures used: db).
        pytest_fixture_scope/test_sample.py::test_non_empty_again (fixtures used: db)F
    TEARDOWN M db

なお、test_non_empty_again(db)assert len(db) == 3のように変更すると、テストは成功します。

classスコープ

classスコープでは、セットアップとティアダウンはクラスごとに実行されるようになります。

import pytest


@pytest.fixture(scope="class")
def db():
    db = []
    yield db
    del db

class TestSampleClass1:
    def test_empty(self, db):
        assert len(db) == 0


    def test_non_empty(self, db):
        db.append("dog")
        db.append("cat")
        assert len(db) == 2

class TestSampleClass2:
    def test_non_empty_again(self, db):
        db.append("cat")
        assert len(db) == 1
        # assert len(db) == 3  # このテストは失敗する

--setup-showオプションを使ってテストのセットアップとティアダウンの順序を確認すると、確かにdbのセットアップとティアダウンがクラスごとに1回だけ実行されていることがわかり、、SETUP C dbのようにクラススコープであることがわかります。

test_sample_class.py 
      SETUP    C db
        pytest_fixture_scope/test_sample_class.py::TestSampleClass1::test_empty (fixtures used: db).
        pytest_fixture_scope/test_sample_class.py::TestSampleClass1::test_non_empty (fixtures used: db).
      TEARDOWN C db
      SETUP    C db
        pytest_fixture_scope/test_sample_class.py::TestSampleClass2::test_non_empty_again (fixtures used: db).
      TEARDOWN C db

packageスコープ

packageスコープを指定すると、セットアップとティアダウンはパッケージごとに1回だけ実行されるようになります。例えば以下のような構造のパッケージがあるとします。

test_sample_package
├── conftest.py
├── test_sample1.py
└── test_sample2.py

conftest.pyは以下のようになっています。

import pytest

@pytest.fixture(scope="package")
def db():
    db = []
    yield db
    del db

test_sample1.pyは以下のようになっています。

def test_empty(db):
    assert len(db) == 0

def test_non_empty(db):
    db.append("dog")
    db.append("cat")
    assert len(db) == 2

test_sample2.pyは以下のようになっています。

def test_non_empty_again(db):
    assert len(db) == 2

--setup-showオプションを使って実行順序を確認すると、確かにdbのセットアップとティアダウンがパッケージ全体で1回だけ実行され、テストはすべて成功していることがわかります。また、SETUP P dbのようにパッケージスコープであることがわかります。

test_sample_package/test_sample1.py 
  SETUP    P db
        pytest_fixture_scope/test_sample_package/test_sample1.py::test_empty (fixtures used: db).
        pytest_fixture_scope/test_sample_package/test_sample1.py::test_non_empty (fixtures used: db).
test_sample_package/test_sample2.py 
        pytest_fixture_scope/test_sample_package/test_sample2.py::test_non_empty_again (fixtures used: db).
  TEARDOWN P db

sessionスコープ

sessionスコープでは、セットアップとティアダウンはセッションごと(pytestを実行するとき)に1回だけ実行されるようになります。

注意点

  • fixtureが他のfixtureに依存する場合は、依存先のfixtureのスコープが依存元のfixtureのスコープと同じかそれよりも広いスコープである必要があります。
  • この記事では、test_sample1.pyの後にtest_sample2.pyが実行されるなどように、テストの実行順に依存してtest_non_empty_againが成功する例があります。わかりやすさのためにこのような例にしていますが、本来、テストは実行順序に依存させるべきではないため、このような使い方は避けるべきです。
  • 説明のしやすさのために、function → module → class → package → sessionの順に説明しましたが、スコープの広さは、小さい順にfunction → class → module → package → sessionです。

参考

  • Brian Okken. テスト駆動Python 第2版 (Japanese Edition)
GitHubで編集を提案

Discussion