Sphinx 拡張のテストを書く

今日の Sphinx+翻訳 Hack-a-thon のテーマとして、Sphinx 拡張のテーマを書くというのをこつこつやってました。
いままで sphinxcontrib.blockdiag に始まり、sphinxjp.shibukawa に至るまでいくつも Sphinx 拡張を書いてきたのですが、
よく考えるとまともなテストを書いたことが一度もありませんでした。

もう 2013年も終わるというのにテストのないコードを書いているなんて、いつホームから突き落とされても文句はいえませんね。

Sphinx 拡張のテストを調べる

年末の忙しい時に人身事故で迷惑をかける前に、テストの書き方を調べてみました。

僕もいくつかコミットしている、Sphinx 拡張の公開リポジトリ sphinx-contrib ではどうなっているでしょう。
sphinx-contrib に登録されている Sphinx 拡張 55個中、テストのあるもの*1は以下の 11個でした。

拡張名 テストコード テスト方法
erlangdomain なし Sphinx で実際にビルドする
httpdomain なし Sphinx で実際にビルドする
phpdomain なし Sphinx で実際にビルドする
rubydomain なし Sphinx で実際にビルドする
doxylink あり ロジック部分のみテストしている(Sphinx との結合なし)
exceltable あり ロジック部分のみテストしている(Sphinx との結合なし)
napoleon あり ロジック部分のみテストしている(Sphinx との結合なし)
feed あり Sphinx との結合テストを行っている。util.py (後述)を利用
mockautodoc あり Sphinx との結合テストを行っている。独自実装(FakeSphinx派)
plantuml あり Sphinx との結合テストを行っている。独自実装(FakeSphinx派)
sadisplay あり Sphinx との結合テストを行っている。独自実装(FakeSphinx派)

整理してみると 4つが Sphinx でビルドできるようテストプロジェクトを配置しているだけでした。
4つともドメイン系の拡張なのでこれでよいのかもしれません。

3つ Sphinx と直接関わらないロジック部分のテストを行っています。
しかし、肝心の Sphinx との連携部分のテストが存在しないため、繋ぎの部分は検証されていないことになります。

最後の 4つが Sphinx 本体と結合して、実際にビルドを行うコードがあります。
feed は Sphinx 本体に含まれている util.py をコピーしたものを使ってテストを書いています。
plantuml と sadisplay、mockautodoc は FakeSphinx というクラスを作って結合をしています。
FakeSphinx は「reST 解釈後のノード」と「出力後のファイル」の 2パターンでテストが書けるようになっています。
同じ実装をつかっているので、どちらかが参考にしたのかもしれません。

というわけで、sphinx-contrib の中でもテストの書き方は統一的なものは存在しないようですね。

Sphinx 本体のテストを調べる

Sphinx 本体については、ややカオスな状況になっています。

Sphinx 本体には 17個の Sphinx 拡張が同梱されているのですが、
明示的にテストが書かれているものは 6つ(autodoc, autosummary, coverage, doctest, linkcode, intersphinx)で、
残り 11個についてはテストが用意 されていません。

これらについては、tests/util.py を利用したテストとなっています。
tests/util.py では @with_app デコレータを利用し、パラメータを切り替えながらテストできるようになっています。
ただ、パラメータが多くて複雑すぎることと、ほとんどのテストが tests/root/ ディレクトリのドキュメントをビルドするようになっているため、
tests/root/ ディレクトリの中身がすごいことになっているという問題があるようです。

また、detox を使ってテストを並列実行するために run.py でちょっとトリックを仕組んでいるとのことです。

util.py を使う?

util.py を使うことを検討したのですが、次の問題があります。

  • util.py は tests/ ディレクトリ内にあり、外から import することができない
    • いまのところコピーするしかない。
    • import できるようにしようよってイシューが立ち上がっている
    • Sphinx がテスト用に用意しているもので、インターフェースが変わらないという保証はない
  • tests/path.py に依存している
    • pathlib っぽい動きをするモジュール
    • これも path / subdir として join するようになっている…
  • 並列実行するのに向いていない
    • 出力ディレクトリが被ると、上書き/削除合戦になる
    • テスト終了時に出力ディレクトリを消すので、原因不明の失敗を起こしやすい
    • テストランナーの run.py をうまく取り込む必要がある
  • root/ ディレクトリが魔境になっている

結局どうしたのか

util.py を参考に、薄く Sphinx コアをラップするテストモジュールを作りました
取りあえず動くことをゴールにしたので、車輪の再発明かつ必要十分ではないという残念感漂うやつです。

@shimizukawa さんもこんなことを言ってるので、だれかなんとかして一緒にかんがえましょう。

*1:test/, tests/ ディレクトリの有無で判断