Skip to main content

第28章:互換性テスト①:観点(何が壊れやすい?)🔍🧪

Test Checkpoints

28.0 この章でできるようになること 🎯✨

この章が終わるころには、こんなことができるようになります😊💡

  • 変更が入ったときに「どこが壊れやすいか」を観点でパッと洗い出せる🔎
  • “互換性テスト”で見るべきポイントを、漏れなくチェックリスト化できる✅
  • 変更内容から「影響」と「テスト観点」を表(マトリクス)で整理できる📋✨
  • 型(コンパイル)だけじゃなく、実行時・データ・意味(セマンティクス)も含めて守れる🛡️

28.1 互換性テストってなに?🧪🤝

互換性テストはひとことで言うと、

「契約(約束)を守れてるか」を、変更のたびに確認するための“見る観点”のセットです🔍✨

ここで大事なのは、テストの種類(ユニット/統合/CDC)より先に、

**「どこが壊れやすいか」を見抜く目(観点)**を持つこと👀💕

その観点があると、レビューでもCIでも、チェックが速くて強くなります💪🔥


28.2 “壊れ方”はだいたい5種類に分けられる🧩🧨

互換性が壊れるときって、だいたいこの5パターンに落ちます👇✨

  1. 型で壊れる(コンパイルで落ちる) 🟦💥

    • 例:関数が消えた、引数が変わった、exportが変わった
  2. 実行で壊れる(ランタイムで落ちる) 🧯😱

    • 例:例外が増えた、Promiseがrejectする条件が変わった
  3. データで壊れる(JSON/DTO/イベントpayloadが合わない) 🧾💔

    • 例:必須項目が増えた、enumの値が変わった
  4. 意味で壊れる(静かに挙動が変わる=一番怖い) 🫥⚠️

    • 例:同じ入力で違う結果、単位が変わった、並び順が変わった
  5. 配布/解決で壊れる(読み込み方が変わって動かない) 📦🧨

    • 例:ESM/CJS、exports、エントリポイント、tsconfigのモードで崩れる TypeScript 5.8では --module node18 のような“固定点”が入り、モジュール解決の差分が意識しやすくなっています。(TypeScript) TypeScript 5.9では import defer も入り、モジュールの評価タイミングまで意識領域に入ってきます。(TypeScript)

💡ポイント:互換性テストは「②〜⑤」を拾えるかが勝負です🔥 型は強いけど、型だけでは守りきれない場面が普通にあります😵‍💫


28.3 互換性テストの“観点チェックリスト” ✅🔍(超重要)

ここからが本題です💖 変更が入ったら、まずこの観点でスキャンします🕵️‍♀️✨

A. 公開API面(Public Surface)🎭🚪

**「利用者が触れる場所」**が変わってないか?

  • exportされてる関数/クラス/型が消えてない?🧹
  • 名前変更してない?(renameは高確率で破壊)✂️
  • exports/エントリポイント(importパス)が変わってない?📦
  • HTTPなら:URL/メソッド/パスパラメータ/クエリ名が変わってない?🌐
  • イベントなら:イベント名が変わってない?📣

✅ テスト観点の例

  • 「以前の使い方(import + 呼び出し)がそのまま通るか」

B. 入力の契約(引数・パラメータ・バリデーション)🧾🧷

**「渡し方」**が変わると壊れます😱

  • 必須引数が増えた?(破壊になりやすい)🧨
  • optional → 必須 に変えた?(ほぼ破壊)💥
  • 型を狭めた?(string | numberstring など)✂️
  • 受け付ける値の範囲が変わった?(例:空文字NGにした)🚫

✅ テスト観点の例

  • 「古い入力(以前OKだった値)が今も通るか」

C. 出力の契約(戻り値・レスポンス・payload)🎁📦

**「返ってくるもの」**が変わると、呼び出し側が壊れます💔

  • 戻り値の型が変わった?(nullable追加/削除)🧊
  • Promiseになった / 同期になった(超破壊)⏱️💥
  • 配列の要素の並び順が変わった?(静かに壊れる)🫥⚠️
  • JSONのプロパティ名が変わった?(renameは危険)🧾💣
  • enumの値が増えた/減った/変わった?🔢

✅ テスト観点の例

  • 「古い呼び出し側が期待する形で返るか」
  • 「増えたフィールドを無視しても動くか」

D. エラーの契約(例外・エラー形式・ステータス)😵‍💫📛

エラーも契約です。ここが変わると運用で燃えます🔥

  • 例外を投げる条件が増えた?😱
  • エラーコード/メッセージの形式が変わった?🧾
  • HTTPのステータス(400/401/403/404/409/500…)が変わった?🌐
  • try/catch の前提が変わった?🧯

✅ テスト観点の例

  • 「以前の失敗ケースで、同じ分類(コード/種別)で失敗するか」
  • 「呼び出し側が分岐できる情報が残ってるか」

E. 意味(セマンティクス)の契約 🧠🫥⚠️

ここが一番やばいです😇(テストしないと気づけない)

  • 同じ入力で、結果の意味が変わってない?(単位、丸め、境界条件)📏

  • 副作用が変わってない?(ログ、イベント発火、キャッシュ)🌀

  • “暗黙の保証”が崩れてない?

    • 例:「この関数はDBを書き換えない」
    • 例:「このAPIは冪等」
  • タイミングが変わってない?(遅延、評価順、非同期化)⏳ TypeScript 5.9の import defer は「いつ実行されるか」に手を入れる文法なので、影響の観点として知っておくと強いです。(TypeScript)

✅ テスト観点の例

  • 「期待する不変条件(invariant)が保たれてるか」
  • 「副作用が増えてないか」

F. 配布・モジュール解決・ビルドの契約 🧱📦

地味だけど事故が多いゾーンです😵‍💫💣

  • ESM/CJSの扱い、exports マップ、型定義の出し方が変わった?📦
  • moduleResolutionmodule のモード差で壊れてない? TypeScript 5.8の --module node18nodenext と挙動差が明記されていて、互換性チェックの観点にしやすいです。(TypeScript)
  • “型テスト”が必要な変更じゃない?(型推論、オーバーロード、条件付き型)🟦

✅ テスト観点の例

  • 「代表的な利用者の import パターンでビルドが通るか」

28.4 「変更 → 影響 → テスト観点」表テンプレ 📋✨

変更が来たら、まずこの表を1枚作るのが最強です🧠💪 (レビューの質が爆上がりします🔥)

変更(What)🛠️困る人/壊れ方(Impact)😱観点タグ(Where)🏷️最低限見るテスト(How)🧪
例:getUser(id)getUser(userId) に改名import/呼び出しが即死💥A 公開API型(呼び出し)+ユニット
例:レスポンスに status を追加古いクライアントは無視できればOKC 出力既存レスポンス互換
例:age: number を必須に古い送信が400になる🔥B 入力 / D エラー旧入力で通るか
例:並び順を降順に変更画面が静かにおかしくなる🫥E 意味並び順の期待テスト

🏷️観点タグのおすすめ

  • A 公開API面
  • B 入力
  • C 出力
  • D エラー
  • E 意味
  • F 配布/解決

28.5 “壊れやすい変更”あるある20連発 💣😇

直感で危険度を付けられるようになると強いです💪✨

💥危険度MAX(だいたい破壊)

  • exportの削除・rename(A)🧨
  • 必須引数の追加(B)💥
  • 戻り値の同期/非同期変更(C)⏱️💣
  • エラー形式の変更(D)📛
  • JSONの必須項目追加(B/C)🧾🔥
  • enumの既存値変更(C)🔢💥

⚠️危険(静かに壊れやすい)

  • 並び順の変更(E)🫥
  • デフォルト値の変更(E)🎛️
  • “意味は同じ”と言い張りたくなる微妙な仕様変更(E)😇
  • バリデーション強化(B/D)🚫

✅比較的安全(でも観点チェックはする)

  • 任意フィールドの追加(C)➕
  • enumの値追加(C)➕(ただしswitch漏れ注意⚠️)
  • 新しいAPIを追加(A)✨

28.6 具体例①:TypeScriptライブラリの変更(型は通る?)🟦📦

変更内容 🛠️

parseOptions の返り値にフィールド追加、そして引数を少し整理したい…!

Before

export type Options = {
mode?: "fast" | "safe";
};

export function parseOptions(input: string, opts?: Options): {
ok: true;
value: number;
} | {
ok: false;
reason: string;
};

After(やりたい変更)

export type Options = {
mode?: "fast" | "safe";
// 追加:ログを取りたい
traceId?: string;
};

export function parseOptions(input: string, opts?: Options): {
ok: true;
value: number;
// 追加:メタ情報
meta?: { traceId?: string };
} | {
ok: false;
reason: string;
meta?: { traceId?: string };
};

観点スキャン 🔍

  • A 公開API:関数名/引数の形は維持 ✅
  • B 入力:optsに任意追加 ✅(安全寄り)
  • C 出力:フィールド追加 ✅(無視できれば互換)
  • D エラー:変化なし ✅
  • E 意味:traceIdが意味を変えないなら ✅
  • F 配布:変化なし ✅

“落とし穴”😵‍💫

呼び出し側が reason のみを取り出して JSON.stringify(result) してる場合、payloadが変わることでログ/比較がズレることがあります🫥⚠️ → 「比較・保存・ハッシュ化」してる箇所がないかはE観点でチェック✅


28.7 具体例②:HTTP APIの変更(データとエラーが地雷)🌐🧾💣

変更内容 🛠️

レスポンスに displayName を追加して、入力の name を必須にしたい。

  • ✅「出力追加」は安全寄り(C)
  • ❌「入力必須化」は破壊寄り(B/D)

観点スキャン 🔍

  • B 入力:必須化 → 古いクライアントが400 になりやすい🔥
  • D エラー:400の形式が呼び出し側で扱えるか?
  • C 出力:追加は無視できればOKだけど、クライアントの厳密パース次第⚠️

💡よくある事故:

  • クライアントが「知らないフィールドが来たら落ちる」実装(厳格デコーダ)😇 → 互換性テスト観点に 「追加フィールドが来ても耐えるか」 を入れる✅

28.8 具体例③:イベント(非同期)は“意味”が壊れやすい📣🧠🫥

変更内容 🛠️

UserCreated のpayloadに source を追加したい。

  • C 出力:追加は比較的安全
  • E 意味:sourceの値で既存処理が変わると危険⚠️

観点としては👇

  • 古いConsumerが source を知らなくても動く?(C)
  • 「作成された」定義が変わってない?(E)
  • 再送/重複イベント時の扱いが変わってない?(E:冪等性)

28.9 ミニ演習:変更→影響→観点表を作ろう✍️📋✨

次の変更案を、さっきのテンプレ表に埋めてみてね😊🌸

  1. 関数の戻り値を null を返す仕様にしたい
  2. エラー時に throw ではなく { ok:false } を返したい
  3. JSONの iduserId にrenameしたい
  4. enumに値を1つ追加したい
  5. 並び順を「新しい順」に変更したい

ヒント💡

  • renameはだいたい A or Cで破壊💥
  • 並び順は Eで静かに破壊🫥⚠️

28.10 “型テスト”という観点 🟦🧪(2026の最新寄りトピック)

互換性って「動く/動かない」だけじゃなくて、型の使い心地が変わるのも大問題です😵‍💫

  • 型推論が効かなくなった
  • オーバーロードの選ばれ方が変わった
  • never が出るようになった
  • “型的にはOKだけど、意図と違う型”になった

こういうのは 型テストで守れます🛡️✨

型テストの代表ツール(押さえ)

  • tsd.test-d.ts を書いて expectError などで型の期待を検証できます。(GitHub)
  • TSTychedescribe/testexpect っぽい書き味で型テストを回せます(実行はせず、型チェッカーで判定)。(tstyche.org)

ここでは「観点」として知っておけばOK🙆‍♀️ 実際の最小構成は次章(第29章)でガッツリ組みます✅


28.11 ユニットテストの“土台”も最新寄りに押さえる🧪⚡

ユニットテストの枠組みとしては、近年は Vitest が定番コースになっています(Vite設定と噛み合うのが強い)✨ VitestはViteの解決/変換パイプラインを再利用できる、という思想が公式に説明されています。(vitest.dev) そしてVitest 4.0のリリースも公式にアナウンスされています。(vitest.dev)

(でもこの章は“観点”なので、実装は次章でOKだよ😊🫶)


28.12 AI活用:観点を漏らさないプロンプト集 🤖🧠✨

AIは「観点の列挙」と「影響の想像」がめちゃ得意です💖 (例:GitHubのCopilot系、OpenAI系のコーディング支援 など🪄)

① 変更点から“壊れ方”を列挙してもらう🔍

  • 「この変更で壊れる可能性があるポイントを、A公開/B入力/C出力/Dエラー/E意味/F配布 で分類して箇条書きにして」

② “意味変更”をあぶり出す🫥⚠️

  • 「この変更はセマンティクス(意味)に影響する?影響するなら、利用者視点の事故例を5つ出して」

③ “テスト観点”を表にしてもらう📋✨

  • 「変更内容をもとに『変更→影響→テスト観点→最小テスト案』の表を作って」

④ “古い利用者”を想像してもらう🧟‍♀️➡️🙂

  • 「古いクライアントが使っていそうな呼び出し例を3つ作って。その呼び出しが今も動くか検証観点もつけて」

⑤ 型の互換性を守る観点🟦

  • 「この型変更で、推論・オーバーロード解決・ユニオンの縮小/拡大がどう変わる?破壊の可能性を列挙して」

28.13 よくある落とし穴 😵‍💫🕳️(観点で防げる!)

  • “追加だから安全”と思って必須にしてしまう(B/Cで破壊)💥
  • “意味は同じ”と言いながら、単位・丸め・境界が変わる(Eで静かに破壊)🫥
  • エラー形式を変えて、呼び出し側の分岐が死ぬ(Dで炎上)🔥
  • 配布設定(exports/モジュール)で import が死ぬ(Fで事故)📦💣
  • 型が通るからOKと思って、ランタイムが落ちる(B/Dで事故)😇

28.14 この章のまとめ 🧁✨

互換性テストは「テストコードを書く前に、どこが壊れるかを見抜く観点づくり」でした🔍🧪 次の章では、この観点を “最小のテスト構成(型+ユニット)” に落として、CIで守れる形にしていきます✅✨