nikkie-ftnextの日記

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

Sphinx拡張のE2Eを書くのにとても役立ちそうなマーカー @pytest.mark.sphinx を知りました

はじめに

あのはな再放送! nikkieです

Sphinx拡張のテストコードを劇的に簡単に書けるpytestのマーカーを知ってしまいました...

目次

自作Sphinx拡張とE2E

SphinxでビルドしたHTML中の、外部ページへのリンクを、ブラウザの別のタブで開くようにする拡張を自作しています。
GitHub - ftnext/sphinx-new-tab-link: Open external links in new tabs of the browser in Sphinx HTML documents

PyPIで公開するパッケージにはテストコードを用意することを自分ルールとしており、この拡張にもテストコードを書きました。
テストでもSphinxを動かしてreSTファイルからHTMLをビルドしています。
このことを指してE2Eと書きました(End-to-Endテスト)

PyCon JP 2022のattakeiさんのスライドより

このE2Eを書くのに私はとても苦労した1のですが、なんとSphinx@pytest.mark.sphinxという、E2E向け超便利マーカーを提供していました!

@pytest.mark.sphinxマーカーを知った経緯

ドキュメント「Testing API

このたびドキュメントを発見しました。

pytestのプラグインとしてconftest.pyに指定します2

pytest_plugins = ('sphinx.testing.fixtures',)

使用方法はSphinxのテストコードが案内されます。
それでは見ていきましょう

Sphinxのテストコード

pytest_pluginsに指定済みです。
https://github.com/sphinx-doc/sphinx/blob/v8.0.2/tests/conftest.py#L35

具体的なテストコードとして、(関心を引いた)test_api_translator.pyを見ました。
https://github.com/sphinx-doc/sphinx/blob/v8.0.2/tests/test_writers/test_api_translator.py

そこで見つけたのが、今回のマーカーです!

# https://github.com/sphinx-doc/sphinx/blob/v8.0.2/tests/test_writers/test_api_translator.py#L24-L29
@pytest.mark.sphinx('html', testroot='api-set-translator')
def test_html_with_set_translator_for_html_(app):
    # use set_translator()
    translator_class = app.builder.get_translator_class()
    assert translator_class
    assert translator_class.__name__ == 'ConfHTMLTranslator'

testrootに指定しているのは、roots/test-api-set-translatorディレクトリ!
https://github.com/sphinx-doc/sphinx/tree/v8.0.2/tests/roots/test-api-set-translator
ディレクトリ名先頭の「test-」は省略できるのだと理解しました。

appsphinx.testing.fixturesが提供するフィクスチャの1つ。
Sphinxアプリケーションを表します。

これをテスト向けの設定で動かすために、@pytest.mark.sphinxマーカーを指定するのです!
逆に言えば、このマーカーを付けるだけなんです!

@pytest.mark.sphinxマーカーを使ってテストを書く

テストコードの検証に使った環境

conftest.pypytest_pluginsを設定したあとは、マーカーの一覧に表示されます。

% pytest --markers

@pytest.mark.sphinx(buildername="html", *, testroot="root", srcdir=None, confoverrides=None, freshenv=False, warningiserror=False, tags=None, verbosity=0, parallel=0, keep_going=False, builddir=None, docutils_conf=None): arguments to initialize the sphinx test application.

テストの実装としては、tests/test-defaultディレクトリを用意して

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

test_example.pyに書きました。

import pytest


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

    assert (app.outdir / "index.html").exists()
    # この記事では省略しますが、BeautifulSoup4でHTMLをパースして検証できる

pytest -sv tests/test_example.pyで実行すると、test-default下のreSTファイルからHTMLをビルドしている様子が確認できます

  • test-default下のreSTファイルは一時ディレクトリにコピー
  • 一時ディレクトリのreSTファイル一式を指定して、make html相当のビルド実施
  • 一時ディレクトリ下にHTMLファイルが生成される(直近数世代を保持するため、開いて確認も可能)

このマーカーについてのドキュメンテーションを私の積ん読リストとして示します。

終わりに

Sphinx拡張のE2Eを書く際には@pytest.mark.sphinxマーカーがとてもよさそうです。

  • conftest.pypytest_plugins"sphinx.testing.fixtures"を指定(フィクスチャやマーカーが有効に)
  • ビルドするreSTファイルはroots/test-hameggのように配置(最小限のSphinxプロジェクト)
  • @pytest.mark.sphinx("html", testroot="hamegg")とマーカーを指定すると、appフィクスチャの返り値はマーカーに渡した値で設定されたSphinxアプリケーション
    • pytest -sと実行すると、テストで生成されたHTMLの保存場所を確認できる

これまで私が書いていたE2Eのコードのほとんどは、このマーカーを使うだけで書かなくてよくなります
今回は知った感動で書き上げましたが、次は互換かどうかを確認して乗り換えたいと思います


  1. pytestのフィクスチャを理解できたのでいい思い出ではあります
  2. https://docs.pytest.org/en/stable/reference/reference.html#globalvar-pytest_plugins