はじめに
チーフとPと応援上映ご一緒した〜、楽しかった〜!! nikkieです。
エミリいいいいいいいいいいいい!!!!(※心の絶叫)
Sphinxのテストに関するネタです。
Sphinx拡張のテストをリファクタリングしたいと思い始めたのですが、そもそも自分で書いたコードが何をやってるかがさっぱり分からなかったので、まずは表題の疑問に取り組みました。
目次
Sphinx拡張のテスト
自作のSphinx拡張にはテストコードを書いて、テストが通ることを確認してから公開するようにしています。
#pyconjp 出逢いと発見、 @attakei さんのSphinx拡張のスライド(とお話)と公開されているコードを元に、E2Eテストが書けました🙌
— nikkie / にっきー (@ftnext) 2022年10月25日
Sphinx拡張のアイデアは温めていたんですが、ずっと"テストってどう書けばいいかわからん"がブロッカーでした。
ありがとうございます!❤️https://t.co/dVon73l8AU
Sphinx拡張のテスト1を書く際に参考にしているのは、PyCon JP 2022のattakeiさんの発表スライド:
def test_default(app: SphinxTestApp, status: StringIO, warning: StringIO): app.build() # assert
みようみまねでテストを書いたのですが、テストの関数の引数にあるapp
やstatus
やwarning
が何か、全く分かっていませんでした。
app
やstatus
やwarning
は、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を覗きに行って分かったのですが、app
やstatus
やwarning
はpytestのフィクスチャです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_1
でawesomeフィクスチャを使う実装- 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が自動でやっている)
以上の検証から、仮説を採用しました。
フィクスチャと分かったので、上で引用したテストコードからは不要なフィクスチャstatus
とwarning
を削除しました。
スッキリ書けています
def test_internal_link_should_not_open_new_tab( - app: SphinxTestApp, status: StringIO, warning: StringIO + app: SphinxTestApp ):
終わりに
pytestで書いたSphinx拡張のe2eテストで、引数に渡すapp
やstatus
やwarning
が何かを見てきました。
これらは sphinx.testing.fixtures
に定義されているフィクスチャ です。
一般名詞なフィクスチャ名でした。
sphinx.testing.fixtures
の実装を理解したく、あるフィクスチャf1の引数に別のフィクスチャf2が指定されて定義されている場合、f2 -> f1の順で実行されることも確認しました。
pytestのフィクスチャは、数珠つなぎのようにできるみたいですね!
詳細を知りすぎなくなるのがよさそうなので、練習してみよう
- 発表スライドにあるように、e2e(=end-to-end)です↩
- プルリクエストをいただき、先日v0.2.0をリリースしました ↩
- フィクスチャは最近のこちらで少しだけ言及しました。 pytestのフィクスチャを初めて聞いた場合は、『テスト駆動Python 第2版』がオススメです↩