nikkie-ftnextの日記

イベントレポートや読書メモを発信

Sphinx拡張のテストをpytestで書いたとき、テスト関数の引数に渡すappやstatusやwarningってなに?

はじめに

チーフとPと応援上映ご一緒した〜、楽しかった〜!! nikkieです。
エミリいいいいいいいいいいいい!!!!(※心の絶叫)

Sphinxのテストに関するネタです。
Sphinx拡張のテストをリファクタリングしたいと思い始めたのですが、そもそも自分で書いたコードが何をやってるかがさっぱり分からなかったので、まずは表題の疑問に取り組みました。

目次

Sphinx拡張のテスト

自作のSphinx拡張にはテストコードを書いて、テストが通ることを確認してから公開するようにしています。

Sphinx拡張のテスト1を書く際に参考にしているのは、PyCon JP 2022のattakeiさんの発表スライド:

def test_default(app: SphinxTestApp, status: StringIO, warning: StringIO):
    app.build()
    # assert

みようみまねでテストを書いたのですが、テストの関数の引数にあるappstatuswarningが何か、全く分かっていませんでした。

appstatuswarningは、pytestのフィクスチャ

自作拡張sphinx-new-tab-link2を例にします。
https://github.com/ftnext/sphinx-new-tab-link/blob/v0.2.0/tests/sphinx_new_tab_link/test_build_html.py#L53-L65

@pytest.mark.sphinx("html", testroot="default")
def test_internal_link_should_not_open_new_tab(
    app: SphinxTestApp, status: StringIO, warning: StringIO
):
    app.build()
    html = (app.outdir / "index.html").read_text()
    soup = BeautifulSoup(html, "html.parser")
    references = soup.find_all("a", {"class": "reference"})

    ref = references[-1]
    assert "internal" in ref["class"]
    assert "target" not in ref.attrs
    assert "rel" not in ref.attrs

このテストを動かすために、conftest.pyを用意しています。
https://github.com/ftnext/sphinx-new-tab-link/blob/v0.2.0/tests/conftest.py#L4

pytest_plugins = "sphinx.testing.fixtures"

この設定により、sphinx.testing.fixturesが有効になります(ref:『テスト駆動Python 第2版』15.4)

sphinx.testing.fixturesを覗きに行って分かったのですが、appstatuswarningpytestのフィクスチャです3

app

https://github.com/sphinx-doc/sphinx/blob/v7.2.6/sphinx/testing/fixtures.py#L132-L150

@pytest.fixture()
def app(test_params: dict, app_params: tuple[dict, dict], make_app: Callable,
        shared_result: SharedResult) -> Generator[SphinxTestApp, None, None]:
    """
    Provides the 'sphinx.application.Sphinx' object
    """

status

https://github.com/sphinx-doc/sphinx/blob/v7.2.6/sphinx/testing/fixtures.py#L153-L158

@pytest.fixture()
def status(app: SphinxTestApp) -> StringIO:
    """
    Back-compatibility for testing with previous @with_app decorator
    """
    return app._status

warning

https://github.com/sphinx-doc/sphinx/blob/v7.2.6/sphinx/testing/fixtures.py#L161-L166

@pytest.fixture()
def warning(app: SphinxTestApp) -> StringIO:
    """
    Back-compatibility for testing with previous @with_app decorator
    """
    return app._warning

pytestのフィクスチャは、引数にある別のフィクスチャを実行できる

pytestのフィクスチャということが分かりましたが、フィクスチャの引数がどう動くのか分かりませんでした。
フィクスチャを利用するtest_internal_link_should_not_open_new_tabなどで引数を渡すわけではありません。

フィクスチャappを例にすると、app引数test_paramsなど)もpytestのフィクスチャです。

  • test_params
  • app_params
  • make_app
  • shared_result

ここから「テストコードでappフィクスチャを使うとき、appの引数のtest_paramsなどがまず実行される」という仮説を持ちました。
検証してみましょう。

引数にある別のフィクスチャを実行する例

pytest 7.4.2を使っています。
絶対に通るテストを追加しました(今回知りたいのはフィクスチャの動きです)

def test_1_equals_1():
    assert 1 == 1

フィクスチャは実行時に単にprintするだけとします

  • test_1_equals_1でcommonフィクスチャを使う実装
  • test_1_equals_1awesomeフィクスチャを使う実装
    • awesomeフィクスチャはcommonフィクスチャを引数に持つので、先の仮説によりcommonも実行されるはず。

pytest -svによる実行結果です。

tests/test_play_fixtures.py::test_1_equals_1_using_common Run common fixture
PASSED
tests/test_play_fixtures.py::test_1_equals_1_using_awesome Run common fixture
Run awesome fixture
PASSED
  • test_1_equals_1_using_commonはcommonフィクスチャだけ実行
    • 出力は「Run common fixture」のみ
  • test_1_equals_1_using_awesome
    • まず「Run common fixture」を出力
    • 次に「Run awesome fixture」を出力
    • つまり、フィクスチャの引数で指定されている別のフィクスチャと辿って行って、引数のないフィクスチャから実行していそう(pytestが自動でやっている)

以上の検証から、仮説を採用しました。

フィクスチャと分かったので、上で引用したテストコードからは不要なフィクスチャstatuswarningを削除しました。
スッキリ書けています

def test_internal_link_should_not_open_new_tab(
-    app: SphinxTestApp, status: StringIO, warning: StringIO
+    app: SphinxTestApp
):

終わりに

pytestで書いたSphinx拡張のe2eテストで、引数に渡すappstatuswarningが何かを見てきました。
これらは sphinx.testing.fixturesに定義されているフィクスチャ です。
一般名詞なフィクスチャ名でした。

sphinx.testing.fixturesの実装を理解したく、あるフィクスチャf1の引数に別のフィクスチャf2が指定されて定義されている場合、f2 -> f1の順で実行されることも確認しました。
pytestのフィクスチャは、数珠つなぎのようにできるみたいですね!
詳細を知りすぎなくなるのがよさそうなので、練習してみよう


  1. 発表スライドにあるように、e2e(=end-to-end)です
  2. プルリクエストをいただき、先日v0.2.0をリリースしました
  3. フィクスチャは最近のこちらで少しだけ言及しました。 pytestのフィクスチャを初めて聞いた場合は、『テスト駆動Python 第2版』がオススメです