背景
テストを書くのが後回しになりがちで、いつも実装が終わってから慌てて足しています。
今日もそうでした。
そして案の定、テストを書いた瞬間に落ちました。
しかも落ちたのはテストの中身ではなく、ファイルを読み込んだ時点です。
原因が地味に面白かったので、備忘録として残します。
前提:テストハーネスが何をしているか
私のサイトのツールは、1ツール1枚の HTML に <script> を直書きするだけの作りです。
ビルド工程を持たず、public/tools/<名前>/index.html がそのまま本番になります。
素朴で気に入っているのですが、そのままだと中の関数をテストできません。
そこで、HTML から <script> の中身を取り出して Node の vm モジュール(文字列を JavaScript として実行するための標準モジュール)で走らせるハーネスを使っています。
ブラウザの API は全部スタブです。
document.getElementById() は「何を呼んでも例外を出さない」オブジェクトを返します。
if (["value", "textContent", "innerHTML", "id", "href"].includes(prop)) return "";
if (["checked", "disabled"].includes(prop)) return false;
これは実際のハーネスの一部で、prop はアクセスされたプロパティ名です。
value を読むと空文字が返る、という点をよく見てください。ここが今日の落とし穴でした。
落ちたコード
追加していたのは、モードを選ぶ <select> を持つ計算ツールです。
モードごとに入力欄のラベルを差し替えたかったので、素直にオブジェクトを引いていました。
var LBLS = {
mm2px: { w: '幅(mm)', h: '高さ(mm)' },
px2mm: { w: '幅(px)', h: '高さ(px)' },
eff: { w: '幅(px)', h: '仕上がり幅(mm)' }
};
function labels() {
var m = $('mode').value, L2 = LBLS[m];
$('lw').textContent = L2.w;
$('lh').textContent = L2.h;
}
labels(); // 読み込み直後に一度呼ぶ
$ は document.getElementById の別名、mode は <select> の id です。
ブラウザでは <select> に必ずどれかの <option> が選ばれているので、m は 'mm2px' などになります。
ところがハーネスの中では .value が空文字です。
つまり LBLS[''] が undefined になり、次の行で落ちます。
TypeError: Cannot read properties of undefined (reading 'w')
at labels (dpi/inline.js:41:28)
labels() はスクリプト末尾で1回呼んでいるので、これはテストが1件も走る前に起きます。
node --test の出力は「テストファイルが失敗」としか言わず、書いたはずのアサーションはどこにも出てきませんでした。
直し方
1行です。
var m = $('mode').value, L2 = LBLS[m] || LBLS.mm2px;
|| で既定値に落とすだけ。
これでテストは通りました。
- 修正前:
node --test test/dpi.test.js→ テスト0件・TypeError - 修正後:同じコマンドで5件パス
なぜこれでいいと思うか
「テストのためにコードを曲げた」ようにも見えるので、少し理由を書いておきます。
LBLS[m] が引けないのは、<select> の値が想定外だったということです。
ブラウザでもこれは起こりえます。
<option> を1つ消したのにテーブル側を直し忘れた、といった編集ミスは普通にやります。
そのとき画面が真っ白になるより、既定のモードで表示が続いたほうが親切です。
つまりこれはテスト都合の回避ではなく、未知のキーに対する防御として素直に正しい形だった、という気がします。
ルックアップテーブルを引くコードは既定値を持たせる。
今日それを、テストに教えてもらいました。
対象のツールはこれです。
https://hashitosystem.com/tools/dpi/
DOM をスタブしたテストでは、select や input の値は空文字で返ってくる。だからルックアップテーブルを引く行は必ず既定値でフォールバックさせておく。これを守るだけで、テストが1件も走らないまま落ちる事故は防げます。
本記事はAI補助で執筆した、個人開発の紹介記事です。