テスト499件が通っても、ブラウザを開くまで3件のバグに気づけなかった|Claude Code製ツールの検証記録

Claude活用

Claude Code で作った小さな Web
ツールを公開しました。ユニットテストは499件すべて通っていましたが、公開直前にブラウザで実際に開いたら、テストでは絶対に見つからない不具合が2件出ました。どちらも「表示と中身が食い違う」タイプで、放置すると景品表示法・ステマ規制に触れかねない種類のものです。AI
に書かせたコードの検証で、テストだけでは足りない部分の話をします。

作ったもの:3問で答える証券口座の診断ツール

質問3つに答えると、回答に事実上いちばん合う証券会社を出す静的ツールです。依存ライブラリはゼロ、vanilla
JS だけで動きます。

設計で1つだけ強く決めたのは、広告報酬の有無で順位を変えないことでした。この手の診断ツールは「報酬が高い会社が1位に出る」と読者に思われた時点で価値がゼロになります。そこで、判定ロジックのファイルが報酬関連のデータを参照していないことを、テストで機械的に検査する形にしました。

// テストの中で logic.js のソースを読み、報酬関連の語が出てこないことを検査する
const src = readFileSync(logicPath, "utf8");   // コメントは事前に除去しておく
assert(!/報酬|sponsored/i.test(src),
  "logic.js が報酬関連フィールドを参照している(誠実設計違反)");

「報酬を見ていません」と書くだけなら誰でもできますが、これならコードが変わった瞬間にテストが落ちます。宣言ではなく仕組みで縛るという発想です。

実物はこちらに置いてあります:証券口座かんたん診断(3問)。判定表とテストコードは
GitHub で全部公開しています。

テスト499件が通っても、ブラウザを開くまで分からなかった2件

結論から言うと、ユニットテストは「ロジックが正しいか」しか見ておらず、「画面に何が出ているか」は一切見ていません。当たり前の話ですが、テストの数が増えるほど「もう大丈夫だろう」という気持ちになるので、ここは意識的に切り分ける必要がありました。

1件目:広告が1本もないのに「広告を含みます」と出ていた

ページの先頭に「本ページには広告(アフィリエイトリンク)を含みます」という表示が出ていました。ところがこのツール、公開時点でアフィリエイトリンクは1本も入っていません。全社とも素の公式サイトURLです。

原因は、その文言が HTML
に直接書かれていたことでした。データ側では「この会社は広告ではない」と正しく持っているのに、表示だけが独立して固定されていたわけです。

直し方は、表示をデータから導出するようにしただけです。

// 実際にアフィリエイトリンクを持つ会社が1社でもある時だけ出す
export function hasAffiliateLink(companies) {
  return Object.values(companies).some((c) => c.cta.type === "a8-active");
}

逆方向の間違い(広告なのに広告と書かない)はステマ規制に直接抵触します。今回は逆でしたが、「表示を人間が手で書く」構造そのものが事故のもとで、そこを直さないと提携が決まった時に同じ場所でミスります。

2件目:リンクのバッジと rel 属性が矛盾していた

結果カードには「広告ではありません」というバッジが出ているのに、リンクの HTML
になっていました。sponsored
は「これは広告です」と検索エンジンに伝える属性なので、画面の表示と真逆のことを言っていたことになります。

実害は小さい(過剰申告なので罰されはしない)のですが、同じページの中で矛盾したことを言っているのは単純に品質の問題です。これも実態から導出する形に変えました。

この2件に共通するのは、どちらも「ロジックのバグ」ではなく「事実と表示のズレ」だという点です。ロジックを何回テストしても出てきません。

AI に書かせたコードの検証で、実際に効いた3つ

ユニットテストの上に、次の3つを足すと今回の2件は拾えました。

やること 拾えるもの コスト
ブラウザで実際に最後まで操作する 表示と実態のズレ・レイアウト崩れ 5分
「宣言」をテストで縛る 後から入る規約違反の変更 10行
本番URLで再確認する ローカルとの差・キャッシュ・配信設定 3分

特に効いたのは1つ目です。今回は3つの質問を最後までクリックして、結果カードのバッジ・リンク先・除外理由の表示まで見て初めて気づきました。AI
はテストが通ると「完了しました」と報告してきますが、それは「自分が書いたテストが通った」以上の意味を持ちません。

3つ目も軽視できません。ローカルで直したはずの表示が、ブラウザのキャッシュで古いままになっていて、一度は「直っていない」と誤認しました。本番URLでの確認は、ローカル確認とは別物として1回やる価値があります。

ハマったところ:WordPress の WAF が JavaScript を通さない

このツールは GitHub Pages に置いて、WordPress 側には iframe
で埋め込んでいます。当初は iframe
の高さを中身に合わせて自動調整するつもりでした。

ところが、<script> を含む固定ページを WordPress の REST API
経由で投稿しようとすると、WAF(SiteGuard)が 403 で拒否します。下書きでも同じでした。iframe
自体は通るので、スクリプトだけが弾かれています。

回避策を探すこともできましたが、WAF
は自分で入れたセキュリティ機能なので、それをすり抜ける書き方を探すのは本末転倒です。結局、高さを固定値にして妥協し、1300px
に置きました。

3件目のバグは、公開したあとに出た

ここまでが公開時点の話です。後日、この固定値が本当に足りているのかを測り直したところ、3件目の不具合が出ました。しかも今度は、公開前に自分が測った数字の中にすでに答えが書いてありました。

やったことは単純で、ブラウザの開発者ツールから全ルートを総当たりでクリックさせ、結果画面の高さを幅ごとに記録しただけです。診断は3問なので、到達できる結果は約50通りしかありません。

iframe の実幅 結果画面の最大高さ 1300px に収まるか
1280px(PC) 1271px 収まる(余裕29px)
343px(iPhone 375px 相当) 1643px はみ出す
311px 1766px はみ出す
280px 1973px はみ出す

スマホ幅では、約50通りの結果のうち大半が 1300px
を超えていました
。はみ出していたのは結果カードそのものではなく、その下にある「最初からやり直す」ボタンと、ツール内に置いた注意書きです。iframe
の中はスクロールできるので情報が消えるわけではありませんが、入れ子のスクロールはスマホでは気づかれません。実質的には「無いのと同じ」です。

救いだったのは、同じ注意書きを iframe
の外側(WordPress の固定ページ本文)にも書いていたことでした。NISA口座は1社しか持てないことも、投資助言ではないことも、ページ側に独立して載せてあります。表示が切れても伝えるべきことは伝わる状態だったので、実害は
UX の劣化にとどまりました。重要な文言を1箇所にしか置かない設計だったら、これは事故になっていました

そして一番こたえたのは、公開前に「モバイル最大1558px」と自分で測っておきながら、高さを1300pxに設定していたことです。データは手元にあったのに、数字を突き合わせていませんでした。測ることと、測った値を使うことは別の作業です。

直し方:JavaScript を諦めても、固定値1つに縛られる必要はなかった

自動調整ができない=高さは固定値ひとつ、と思い込んでいたのが間違いでした。CSS
のメディアクエリなら JavaScript なしで画面幅ごとに高さを変えられます
。WAF
が弾くのは <script> であって <style>
ではないので、そのまま通ります。

.shindan-frame { width: 100%; border: 0; height: 1350px; }
@media (max-width: 600px) { .shindan-frame { height: 1700px; } }
@media (max-width: 360px) { .shindan-frame { height: 1900px; } }

数値は勘ではなく、上の実測表から「その幅での最大値+余白」で決めています。完全な自動調整には及ばず、短い画面では下に余白が出ますが、少なくとも内容が可視域の外に出ることは無くなりました。適用後にスマホ幅で再確認し、iframe
が 343px 幅・1700px 高さになって実測最大の 1643px を収容できていることを確かめています。

自動調整を入れ直すつもりは、今のところありません。WAF
を緩めてまで得られるのは「余白が消える」ことだけで、割に合わないからです。ただし診断の設問や表示を増やして高さが伸びたら、この固定値は静かに破綻します。中身を増やしたら測り直す、をセットで運用するしかないと考えています。

まとめ

  • ユニットテストは「ロジックが正しいか」しか見ない。表示と実態のズレは別枠で確認する
  • 「報酬で順位を変えない」のような宣言は、文章で書かずにテストで縛ると後から壊れない
  • AI の「テストが通りました」は、そのAI が書いたテストが通っただけ。ブラウザで最後まで操作するのが一番安い保険
  • WAF に弾かれたら、すり抜け方を探さずに仕様側で妥協する

ツール本体はこちらです:証券口座かんたん診断(3問)

あわせて読みたい

この記事ページの本文にはアフィリエイトリンクは含まれていません(下部の診断ツール内を除く)。診断ツール内のリンクは、A8.net の提携が承認された会社(現時点では松井証券・DMM 株の2社)についてはアフィリエイトリンクになっており、その会社のカードには「PR」バッジと、ツール冒頭に広告表示を出しています。提携していない会社は各社公式サイトへの通常リンクのままです。この記事の本文で「アフィリエイトリンクは1本もない」と書いているのは、あくまで公開当時の状態です。その後この記事自身が題材にした「宣言と実態のズレ」を起こさないよう、提携の追加に合わせてツール側の表示(バッジ・rel・広告表示)を実態連動にしてあります。

コメント

タイトルとURLをコピーしました