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・広告表示)を実態連動にしてあります。

コメント