nikkie-ftnextの日記

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

Sphinx拡張のE2Eを書くときは、conftest.pyにrootdirフィクスチャも定義する

はじめに

うう・・・百合子・・・ nikkieです。

先日の@pytest.mark.sphinxの補完エントリです

目次

まとめ:Sphinx拡張のE2Eを書くには

pytestを使います。

ディレクトリ配置

.
└── tests/
    ├── roots/
    │   └── test-default/
    │       ├── conf.py
    │       └── index.rst
    ├── __init__.py
    ├── conftest.py
    └── test_example.py

conftest.pyでは1

  • pytest_pluginsを指定
  • rootdirフィクスチャを定義
from pathlib import Path

import pytest

pytest_plugins = ["sphinx.testing.fixtures"]
collect_ignore = ["roots"]


@pytest.fixture(scope="session")
def rootdir() -> Path:
    return Path(__file__).parent / "roots"

test-defaultディレクトリのSphinxプロジェクトを元に、HTMLをビルドするテスト(※先日の記事に詳しいです)

import pytest


@pytest.mark.sphinx("html", testroot="default")
def test_example(app):
    app.build()

    assert (app.outdir / "index.html").exists()

コード全容

rootdirフィクスチャの上書き

conftest.pypytest_plugins = ["sphinx.testing.fixtures"]を指定した時点で、sphinx.testing.fixturesが提供するフィクスチャ(やマーカー)が有効になります。
pytest --fixturesの出力を見ると、そこにはrootdirというフィクスチャもあります。

% pytest --fixtures

---------------- fixtures defined from sphinx.testing.fixtures -----------------
rootdir [session scope] -- .venv/lib/python3.11/site-packages/sphinx/testing/fixtures.py:45
    no docstring available

実装を見に行くと
https://github.com/sphinx-doc/sphinx/blob/v8.0.2/sphinx/testing/fixtures.py#L44-L46

@pytest.fixture(scope='session')
def rootdir() -> str | None:
    return None

Noneを返すsessionスコープのフィクスチャです。

conftest.pyに同名のrootdirフィクスチャをsessionスコープで定義することで、Noneではない値(=rootsディレクトリへのパス)を返すようにしたと理解しました。
pytestのフィクスチャのドキュメントには、overrideについての項目があります。
https://docs.pytest.org/en/stable/how-to/fixtures.html#overriding-fixtures-on-various-levels
「Override a fixture on a folder (conftest) level」を見て、

  • sphinx.testing.fixturesにおいてはrootdirフィクスチャはNoneを返す
  • 私たちが置いたconftest.pyディレクトリ以下ではrootdirフィクスチャは代わりにrootsディレクトリへのパスを返す

ということではないかと思いました。

conftest.pyrootdirフィクスチャを定義した後は、同名のフィクスチャが見つかります

% pytest --fixtures

---------------- fixtures defined from sphinx.testing.fixtures -----------------
rootdir [session scope] -- .venv/lib/python3.11/site-packages/sphinx/testing/fixtures.py:45
    no docstring available

--------------------- fixtures defined from tests.conftest ---------------------
rootdir [session scope] -- tests/conftest.py:10
    no docstring available

rootdirフィクスチャは何をしているのか

app_paramsフィクスチャがrootdirフィクスチャを呼び出しています(ドキュメントの言葉だとrequest)。
https://github.com/sphinx-doc/sphinx/blob/v8.0.2/sphinx/testing/fixtures.py#L71-L78

@pytest.fixture
def app_params(
    # ...
    rootdir: str,
):

app_paramsフィクスチャは(上のテストコードで使った)appフィクスチャから呼び出されます。
また、@pytest.mark.sphinxマーカーの処理も担っています。

rootdirフィクスチャが使われるのはこの箇所
https://github.com/sphinx-doc/sphinx/blob/v8.0.2/sphinx/testing/fixtures.py#L109-L112

if rootdir and not srcdir.exists():
    testroot_path = rootdir / ('test-' + testroot)
    shutil.copytree(testroot_path, srcdir)

shutil.copytreeを使って、test-defaultのようなSphinxプロジェクトをsrcdirにコピーします。
srcdirsphinx_test_tempdirフィクスチャにより、一時ディレクトリへのパスとなります。

sphinx.testing.fixturesに定義されたrootdirフィクスチャはNoneを返すので、上書きしない場合Sphinxプロジェクトをコピーするこの箇所は実行されません。
rootdirapp_paramsフィクスチャの定義には必要、しかしその詳細はテストコードを書くユーザに委ねるという実装になっているのですね。

終わりに

Sphinx拡張のE2Eの書き方の理解がだいぶ深まりました。
マーカーに加え、rootdirフィクスチャの定義だけでよいので、かなり簡単にE2Eを書き始められますね!

ユーザに詳細を定義させるフィクスチャというのも興味深いと思いました。

参考文献

コミッターtk0miyaさんによるquick note
https://github.com/sphinx-doc/sphinx/issues/7008#issuecomment-573092764

Sphinxのテストコードより、conftest.py
https://github.com/sphinx-doc/sphinx/blob/v8.0.2/tests/conftest.py#L35-L45


  1. 本文では省略したcollect_ignoreですが、rootsディレクトリ下はコレクト不要とpytestは伝えています。そこにテストケースはありませんからね