🦁

仕様がわからなくてもテストをかける rspec-undefined を作ったよ

に公開

数千のテストパターンがあってもClaude Codeがあれば、テストコードは現実的に書けるようになりました。

しかし、仕様が未定義だったり、振る舞いが不定だったりすると、流石のClaude Code師匠でもテストは書けません。テストをスキップしたり、TODOつけてくれたりと、師匠なりに弟子を路頭に迷わせまいという親心を見せてくれるわけでありますが、結局のところ、お前たちヒューマンが仕様を意思決定せよ、と師匠は仰せなわけです。

私は、ヒューマンとAIが仲良く開発していくことを切に望んでおります。であれば、「AIがテストを書く」と「人間が仕様を決める」という責任を別々のタイミングで全うできる方法がないか、ということで、rspec-undefined を作ってみました。

この rspec-undefined は、仕様の未定義や不定値を明示的にテストコードに書くことができます。この未定義は集計されるようになっています。テストコードを充足させつつ後で仕様を決められるという状態を生み出すことができます。

gem はしばらく作っていなかったので、Claude Code 師匠のお力を存分に使わせていただいております。

https://github.com/tomoyukiinoue/rspec-undefined

簡単な使い方

spec_helper.rb で require するだけで動きます。簡単。

require "rspec/undefined"

マッチャはこんな感じで書きます。
分類と「エンジニアとしてはこうだと思っとる」という暫定の期待値を渡せます。

# 「ここの仕様は未定」とだけ宣言
expect(value).to be_undefined

# カテゴリだけ指定(境界値の仕様が未定)
expect(total).to be_undefined(:boundary)

# 暫定仕様の期待値つき(== 比較)
expect(total).to be_undefined(:boundary, expected: 100)

# RSpec マッチャを expected: に渡すこともできる
expect(users.map(&:id)).to be_undefined(:order, expected: match_array([1, 2, 3]))

expected: に渡した値は 記録されるだけ で、通常モードでは常に pass します。つまり現状の挙動はこう、でも正しい仕様かは未確定という状態をテストコードに残せます。
これこれ。

example レベルで未確定を宣言する DSL もあります。

undefined "削除時の順序は未確定"
undefined "キャンセル後の再操作", category: :state_transition
undefined "検証内容あり" do
  expect(something).to eq(42)
end

テスト実行の末尾に、未確定項目のサマリとカテゴリ別集計が出ます。

Undefined spec items:
  1) [matcher] {boundary} be_undefined expected=:__any__ actual=100 matched=true (spec/user_spec.rb:12)
  2) [declaration] {deletion} 削除時の挙動は未確定 (spec/user_spec.rb:30)

undefined: 2
by category:
  boundary: 1
  deletion: 1

JSON / YAML / Markdown でファイルに出力することもできるので、そのまま仕様確定タスクの一覧として使えます。

仕様確定が進んできたら、CI で RSPEC_UNDEFINED_STRICT=1 を付けてやってください。未確定が残っている example はすべて fail するようになります。「もう未確定は増やさんでくれ」という運用に切り替えるスイッチです。

カテゴリ

仕様未定義はおおむね13種類に分類できるそうで、いつもお世話になっている IPA (情報処理推進機構)さんから引用させて頂きました。

カテゴリ 対象例
:boundary 上限/下限・最大件数・桁数・文字数・期間
:nil_or_empty 0 件・null・空文字・未入力
:uniqueness 一意制約・同名登録・同時登録
:order 並び順・ソート規則
:datetime 日時・タイムゾーン・和暦/西暦・うるう年/秒
:encoding 文字コード・絵文字・サロゲートペア・半角/全角
:rounding 金額丸め(四捨五入/銀行丸め)・通貨・税計算順序
:permission 権限境界(閲覧/編集/削除・代理操作)
:state_transition 状態遷移(キャンセル後再操作・途中離脱・タイムアウト復帰)
:concurrency 楽観/悲観ロック・同時編集コンフリクト
:deletion 物理削除 vs 論理削除・削除済み参照
:retroactive マスタ変更遡及(過去データ表示は旧値か新値か)
:idempotency 外部連携(リトライ・重複実行防止)

この分類、IPAの『ユーザのための要件定義ガイド 第2版 要件定義を成功に導く128の勘どころ』や『非機能要求グレード』で整理されている「要件定義で漏れやすいポイント」を、レガシーシステムの現行踏襲仕様化で何度も出くわす類型に寄せたものです。

胃が痛いですね。

IPA の資料は「言ったつもり・書いたつもり・わかっているはず」を減らせと繰り返しおっしゃっておられますが、レガシーシステム相手だと、そこに「そもそも誰も知らない」という新たな役者が登場します。いないんですけど。ですので、テストコード側に未確定を明示的に置いておける箱 がどうしても要る、というわけです。

もちろん 13 種類に収まらないものも出てきます。プロジェクト固有のカテゴリは register_categories で追加できるようにしています。未登録の Symbol を渡すとレポートで * マーカーが付き、登録忘れに気付ける仕組みです。

RSpec::Undefined.configure do |c|
  c.register_categories :invoice_rounding, :legacy_auth
end

現行踏襲仕様書という考え方

現行踏襲仕様書については、以前に私が書きました。

https://zenn.dev/tokium_dev/articles/a8e7af3930a473#現行踏襲仕様書という考え方

要点は「仕様がわかるから書く、のではなく、コードの現在の振る舞いを事実として書き起こす」というアプローチです。こちらもIPA の『システム再構築を成功に導くユーザガイド(第2版)』で再構築プロジェクトの成功要因として位置付けられています。

このアプローチで一番大事なのは、不明な部分を無理に埋めない ということです。ソート順が未定義なら「ソート順は不定」と書く。解釈に揺れがあるなら「複数の解釈があり得る」と明記する。事実と推測を区別し、不明は不明と明記する。そうすると「不定と書かれた箇所」がそのままモダナイズ時の論点リストになる、という発想です。

ここで AI が効いてきます。AI は「仕様がわからないから書けない」という人間側のジレンマを持ちません。コードを淡々と読んで、事実を事実として記述するのは AI の得意領域です。逆に「ここが本当に正しい仕様か」を決めるのは AI には(少なくとも単独では)できない領域で、そこはヒューマンの仕事として残ります。

まだまだヒューマンは働かなくてはなりませんね。

rspec-undefined は、この「事実として記述する/不明は不明と明記する」をテストコードの語彙として持ち込むためのものです。

  • 現状の挙動はテストに固定する(AI が書ける)
  • でもそれが正しい仕様かは未確定、という印を be_undefined で残す
  • 未確定はレポートに集計される → そのまま論点リストになる
  • 正しい仕様を決めるのは、後でヒューマンがやる

現行踏襲仕様書を「散文のドキュメント」ではなく「実行可能なテストコード+未確定レポート」という形で運用する、というのが狙いでございます。散文の仕様書は読まれぬまま腐りますが、テストは CI で毎日走ります。腐りにくいほうに寄せておこう、という魂胆です。

進め方

ぜひ、この gem を使ってレガシーシステムのテストコードを完成させて下さい。今後ハーネスするなら100%のカバレッジを目指して下さい。
で、ヒューマンは、レポートにある仕様を決めて下さい。
最終的に rspec-undefined が Gemfile から消えれば、未確定の仕様がない状態になっています。

そこからシステム延命するのか、リプレイスするのか、ハーネスを作っていくのか、改善するのか、それを決めるのはヒューマンです。

では。

参考文献

TOKIUMプロダクトチーム テックブログ

Discussion