blog

文字列一致の検査はコメントを見逃す

  • Claude Code
  • テスト
  • リファクタリング

main.tsは起動直後にdocument.querySelectorを呼ぶ。だからテスト実行環境ではそのままimportできない。DOMが無い場所でDOMを触るコードを動かそうとすれば、当然そうなる。

配線が崩れていないかを確かめたいのに、実際に動かして確かめられない。そこで採った手が、ソースコードのテキストを正規表現で読んで「呼んでいるはずの行があるか」を見る検査だった。構えている敵の色を変える処理を呼んでいるか、敵弾の色分けを呼んでいるか、乱数のsaltを渡しているか。同じパターンをこれまでに4回使っていた。

「そう書いてある」ことは確かめられる。「そう動く」かは別

この検査がどこまで守れるのかを、自分で疑ってみることにした。

守りたい行をそのまま削除すれば、正規表現は一致しなくなるので検査は落ちる。ここまでは意図どおりだ。

ところが、その行を//でコメントアウトしても、正規表現は文字列としては一致したままなので検査は通ってしまう。同じようにif (false) { ... }で包んで実行されない状態にしても、ソーステキストの中にはその文字列がまだ存在するので、やはり検査は緑のままだった。

「書いてあるかどうか」と「その通りに動いているかどうか」は別の問いだ。ソース検査ができるのは前者だけで、後者は保証していなかった。自分は後者のつもりでこの検査を書いていたので、これは思っていたより深刻な穴だった。

判断を、DOMを触らない関数へ切り出す

main.tsをimportできない根本の理由は、トップレベルでdocument.querySelectorを呼んでいることにある。ならば、フレームごとに行っている判断そのものを、DOMに触らない別の関数へ切り出せば、その関数だけは普通にimportして動かして試験できる。

構えている敵の色を選ぶ処理は、こうなった。

export function foeMeshColor(foe: { aiming: number }): number {
  return foe.aiming > 0 ? FOE_AIMING_COLOR : FOE_COLOR;
}

main.ts側はこの関数を呼ぶだけにする。呼び出す側は相変わらずソース検査でしか守れないが、呼ばれる側のfoeMeshColorは普通の単体試験で守れる。中身をコメントアウトしてもif (false)で包んでも、この関数を実際に呼んで戻り値を見る試験なら、両方の潰し方でちゃんと落ちることを確認した。

チュートリアルの進行判定(移動した・撃った・漁った・拾った)も、nextTutorialSignalsという関数に切り出して同じ形にした。ただし4つが揃って同じ作りなわけではない。移動と射撃はstepSessionの前後を比べて立てているが、漁ったと拾ったは「開いている物入れがあるか」「いま何か持っているか」を見ているだけで、そのtickで起きた変化ではない。

二重の網として残す

ソース検査を消したわけではない。「関数を呼んでいるか」は引き続きソース検査が守る。呼び出しをコメントアウトされたり、到達しない分岐に入れられたりする可能性は、この検査方式である限り残る(丸ごと削除されたときは、文字列が消えるので落ちる)。ただ、判断の中身が正しいかどうかは、もう文字列一致に頼らずに済むようになった。

見比べてみると、main-wiring.spec.tsが守っている配線5つ(乱数のsalt・移動と射撃・構えの色・敵弾の色・漁った)は、どれも同じ形の検査だった。4回の作業で積み増した穴が、そのまま5つぶん並んでいたことになる。1回目に気づいていれば、2回目以降は最初から関数を切り出す形で書けていたはずだ。ソース検査は「書いてあるか」しか見ない、という前提を毎回忘れて、動作まで守れているつもりになっていた。

緑であることは、検査が正しいことを意味しない。何も見ていなくても緑にはなる。実際に動かして確かめる試験を、判断のいちばん近くに置くしかない。

構えの色や漁る判定はProd Runの画面で確かめられる。

STAGE10/50SCORE0BEST0COMBO×1貯蓄0KILLS0
TAP = FOCUS FIRE
強化ツリー倒した数で買う。1つの強化は1回だけ。枝は左から順に開く。
kill -9 agentKILLS 0
kill -9 agent 停止中