成果物
pytest-rsというpytest互換のテストランナーをRustで作っている。PyPIにも公開済み。
uv add --dev pytest-rs
pytest-rs -n 4 --cov=mypkg
既存のテストコードは無改造で動く。(もし動かなかったら是非issueを!)
現時点でpytest本体のテストスイート97.1%、pandasやfastapiなど実プロジェクトのテストスイートもほぼ99〜100%通ってる(数字の定義は後述)。
やったこと
pytestの起動・収集・fixture解決・カバレッジ計測・レポート出力といった実行部分を、テスト本体はCPythonで動かしたまま、周りを丸ごとRustに置き換えた。設定ファイルもCLIフラグも既存のプラグイン(pytest-mock、pytest-cov、pytest-xdistなど)もそのまま使える、という縛りでやっている。
なぜそんなことをするのか
pytestは便利だけどテストが増えてくると起動やカバレッジ計測がじわじわ重くなる。(実際本業ではcollectのみで1分以上かかる)
速くしたい、でも「pytest互換」を諦めたくない、という欲張りな動機で始めた。
ハマったポイント ~「pytest互換」の正しさをどう担保するか~
実装自体はClaude Codeにかなりの部分を任せている。ただ、AIに大きな実装を書かせるとき一番怖いのは「動いてるように見えるけど実は壊れてる」を検知できないことだった。自分でテストケースを書いて確認するのは簡単だけど、それだと結局「自分が思いついたケースにしか強くない」実装になる。AIが書いたコードをAI自身に「動作確認しました」と言わせても何の担保にもならない。
そこで、pytest自身やpytest-cov/pytest-xdistなど互換対象プラグインのupstreamテストスイート、さらにclick・fastapi・pandas・scikit-learnなど実プロジェクトのテストスイートを一切改造せずにpytest-rs上でそのまま実行するというハーネス(conformance/)を先に作った。pandasだけで約19万件のテストがある。自分で書いたテストじゃなくて、各プロジェクトのメンテナが実際に運用してるテストをそのまま通すので、都合の良いケースだけ選んで通す、ということができない。
スコアはconformant % = (passed + skipped) / totalで定義していて、環境依存で実行自体無効なdeselectedと、既知の理由で失敗するknown_failed(直ったら自動でpassed扱いに戻る)は分けて集計している。
これがあることで実装サイクルが「AIに書かせる → conformanceを回す → pandasの19万件のうちどれが新規に落ちたか見る → 直す → 再実行」という、人間の主観に頼らないループになった。AIが「多分動いてます」と言っても、本物のOSSプロジェクトのテストが1件でも新規に落ちれば機械的にバレる。CIでも同じ仕組みでスコアボードを継続更新していて、pass→failの回帰があれば即検知できる。
所感
実装の大部分をAIに書かせつつ品質を落とさない方法として、「人間がレビューして良さそうと判断する」より「本物の外部テストスイートに継続的に晒す」仕組みを先に用意しておくのがかなり効いた実感がある。pytest-rs自体もまだalpha版でUnix限定・--pdb未対応など粗いところは残っているので、興味あれば触ってみてissue投げてもらえると嬉しい。