テストの防御線
Minibankに実在するテストを、実行層だけでなく『どんな事故を実際に防いだか』から読み解きます。
境界、transaction、並行制御、脅威、テスト、障害対応をFailure Firstで学ぶ。
先に学ぶ内容 — Prerequisite: 基礎 / 銀行システム学ぶこと — LEARNING GOALS
このページで学ぶこと
- Regression・API・E2E・Concurrency・Privilegeの役割
- テストが発見した実バグ3件
- 合格件数が保証する範囲と限界
体験13 · TESTING(テストの防御線)
「一度動いた」と「いつも安全」は同じか確かめよう
まずは予想をひと言。本文を読んだ後、教材内の参照元で実際のテスト記録を確認します。
- 01まず予想する — PREDICT
まずここだけ:思ったことをひと言書く
プログラムが一度正しく動いたら、テストはもう不要でしょうか?
正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。
ヒント1を見る
「正常に動くこと」を確かめるテストと、「わざと失敗させて」確かめるテストは、同じものでしょうか?
ヒント2を見る
同時に2つの処理が重なったときの問題は、順番に1回ずつ試すテストで見つかるでしょうか?
もっと詳しく:この問いで使う専門用語
- Regression test(回帰test)は、過去に守れていた約束が変更後も壊れていないかを確認するtestです。
- concurrency test(同時実行test)は、複数の処理をわざと同時に進行させて競合を確認するtestです。
- failure recovery test(失敗回復test)は、処理の途中でわざと失敗させ、記録が半端に残らないかを確認するtestです。
- 状態機械testは、各状態と許可された遷移が正しく守られているかを確認するtestです。
日常語で理解する — PLAIN LANGUAGE
まず、ここだけつかむ
テストは『正常に振込できる』だけでなく、攻撃・再送・終了状態・途中失敗でも安全かを確かめる防御線です。
実際に、function overloadによるPGRST203、failed OTPをexpiredと誤る状態判定、生DB error漏洩の3件をtestが発見し、再発防止caseとして残しました。
DB/RPC、API、E2E、Privilege、Concurrencyは守る境界が違います。280件や11件という数字だけでなく、何を試していないかも読みます。
順番に理解する — DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
DB regression test
過去に守れていた制約や権限が、変更後も壊れていないかをDBへ直接確認します。
UIテストだけではPUBLIC EXECUTE、RLS bypass、直接UPDATEなどを見落とします。
JavaScript suiteがSupabase clientとCLI queryを使い、会計制約、RLS、関数権限、状態遷移を検査します。
API / E2E test
APIの入口と、ブラウザからの利用者操作をそれぞれ確認します。
DBが安全でもtoken受渡しや入力変換、画面の導線が壊れることがあります。
API回帰とPlaywrightのE2Eが別の境界を検証します。
concurrency test
複数処理を意図的に同時進行させ、lockや一意制約の働きを確認します。
逐次実行ではlost update、deadlock、二重承認などの競合を再現できません。
同一口座への複数送金、同一key、承認と否認などを別connectionから競合させる必要があります。
failure recovery test
処理途中で意図的に失敗させ、半端な記録が残らないかを確認します。
成功経路だけではtransactionとrollbackの保証を証明できません。
仕訳や監査の途中で例外を起こし、業務記録・残高・台帳がすべて元に戻るかを検査対象にします。
状態機械test
各状態と許可された遷移、終了状態の再実行を確認します。
WHERE句で終了行が見えなくなると、not found・expired・failedを取り違えるからです。
transferOtp suiteが誤入力1〜5回、6回目、再発行、期限、owner、consumedを区別します。
発展 — ADVANCEDもっと詳しく確認する最初の学習では読まなくても問題ありません。仕組みをもっと深く理解したい場合に開いてください。
現在のテスト構成
| 種類 | 実装 | 守る境界 |
|---|---|---|
| DB / RPC Regression | JavaScript suite + Supabase CLI / client | constraint、RLS、RPC、状態、冪等性、Ledger、権限 |
| API Regression | scripts/regression/api | Bearer認証、Zod、header、HTTP status、安全なerror body |
| Browser E2E | Playwright full-journey.spec.ts | 顧客・管理者の実画面からDB反映まで |
| Security / Privilege | privilege.mjs、actorSeparation.mjs | function ACL、table privilege、RLS、schema露出、actor兼務 |
| Concurrency | 一部suiteとadhoc multi-session検証 | idempotency・unique・admin TOCTOU等。一律の負荷競合suiteではない |
| Unit | 通常の独立unit test runnerは未確認 | pure functionを細かく隔離する層は現状薄い |
Regression:過去の安全契約を、変更後も守る
回帰testは新機能の確認だけではありません。KYC、口座、Ledger、振込、逆仕訳、admin、audit、reconciliation、権限といった既存契約を、新しいOTPやお気に入り追加後も壊していないか確かめます。Hosted DBへ実dataを作るためrunnerは並列安全ではないと明記され、suiteを直列に進めます。
- 01run-regression.mjsを開始
- 02固定actorとfixtureの前提をassert
- 03run.mjsがDB/RPC suiteを直列実行
- 04run-api.mjsがHTTP API suiteを実行
- 05全結果をDB / API / TOTALで集計
- 06失敗したcase IDとdetailから退行箇所を特定
API test:DBが安全でも、入口が壊れることを防ぐ
API testはRoute Handler、auth context、validation、Service、error変換を確認します。DBへ直接RPCを呼ぶtestとは違い、HTTP境界で利用者に何が見えるかを守ります。
| 確認 | 防ぐ事故 |
|---|---|
| 認証なし・壊れたtoken | 未認証callerがAPIへ到達する |
| request bodyとIdempotency-Key | 不正型、桁あふれ、keyなし、別内容のkey再利用 |
| SQLSTATE mapping | DBの生message、constraint名、内部SQLがclientへ漏れる |
| 他人のresource | IDを直接指定してownershipを突破する |
| Response契約 | Service変更でstatusや公開fieldが意図せず変わる |
E2E:人が操作する一連の旅を確認する
Playwrightのfull-journey.spec.tsは、未認証redirect、customer login、KYC申請、admin承認、口座開設、freeze/unfreeze、customer/admin分離、logoutを実browserで確認します。振込arcは入力→DB解決済み名義の確認→demo OTP→完了→履歴を通り、残高不足では安全な日本語だけが表示されapplication shellが壊れないことを確認します。
| E2Eで分かる | E2Eだけでは分からない |
|---|---|
| 画面・API・Auth・DBがつながる | 全functionのGRANT/REVOKEが正しいか |
| 利用者に見える文言と導線 | あらゆる同時実行race |
| 代表的な成功・失敗journey | DB ownerや全攻撃patternへの耐性 |
| OTP後に履歴へ反映される | すべてのLedger不変条件の網羅 |
Security / Privilege test:見えない権限表を検査する
privilege.mjsはUIを使わずPostgreSQL catalogを読み、公開関数が存在するか、PUBLIC/anon/service_roleへ不要なEXECUTEがないか、authenticatedだけへ必要権限があるかを確認します。主要tableのwhole-table privilege、column privilege、RLS有効化、Data API公開schemaも別々に検査します。
actorSeparation.mjsはactive adminへのcustomer INSERTがAC001で拒否される方向と、customerをadminへ昇格しようとするとAM006で拒否される方向を試し、失敗後にcollision rowが残らないことも確認します。
Case 1:function overload競合をtestが発見
PostgreSQLではfunction名だけでなく引数型の並びもidentityです。引数signature変更は『同じ関数の更新』とは限りません。Migrationではpg_procやPostgREST経由の旧契約も確認する必要があります。
- 01execute_transfer()へdefault付き引数を追加
- 02CREATE OR REPLACE FUNCTIONを実行
- 03旧3引数版は別signatureなので残留
- 043 named argumentsは旧版と新版の両方に一致
- 05PostgRESTが選べずPGRST203
- 06Regressionで全旧callが失敗して発見
- 07旧3引数signatureを明示DROPし1候補へ戻す
旧: execute_transfer(text, bigint, uuid)
新: execute_transfer(text, bigint, uuid, text DEFAULT NULL, ... )
3引数call → 2候補 → PGRST203Case 2:状態機械をWHERE句で消したバグ(#128以前のデモOTP実装)
DBに行は存在していても、WHERE条件で見えなくするとprogramからはnot foundに見えます。状態機械では『存在しない』『終了した』『今も有効』を別の意味として扱い、行を取得してから状態を判断します。transferOtp suiteは5回目と6回目の結果を固定します。
- 01OTPを5回誤入力
- 02challenge.status = failed
- 03旧queryはWHERE status = 'active'
- 04failed行が検索結果から消える
- 05『存在しない』分岐へ入りotp_expired
- 06最新challengeをstatus不問で取得
- 07取得後にfailed / expired / superseded / consumedを分岐
Case 3:生DBエラー漏洩をE2E更新中に発見
修正では内部診断まで捨てず、private tableのfailure_reasonにはraw messageを残します。外部境界にはcodeだけを渡すことで、運用者の調査能力と利用者への情報最小化を両立しています。
- 01OTPは正しいがexecute_transfer()が残高不足
- 02exceptionのmessage_textをfailure_reasonへ格納
- 03同じmessageをRPC responseへ返す
- 04clientへ内部詳細が出る経路になる
- 05RPC returnをfailure_code=SQLSTATEへ変更
- 06mapRpcError()がallowlist済み日本語へ変換
- 07E2Eでraw DB patternがbodyにないことを確認
Concurrency test:順番に2回では再現できない
同時実行testは複数sessionの処理時間を重ね、同じ古い状態を読ませます。Minibankには冪等性unique、口座row lock、決定順lock、admin caller row lockなどのDB対策と一部のmulti-session adhoc検証があります。
一方、adminQuorum suiteは真の同時disable競合を通常Regressionで実行しないと明記しています。全振込・KYC・admin操作のraceを再現する決定的なconcurrency suiteや、長時間の負荷・耐久testが完成済みとは記載しません。
| scenario | 期待する結果 |
|---|---|
| 同一口座から残高を超える2送金 | lock後に片方が残高不足となりoverdraftしない |
| 同一idempotency keyを同時送信 | 実資金移動は1回、別内容ならconflict |
| OTP確認と再発行が競合 | request row lockでchallenge遷移を直列化 |
| 管理者無効化と管理RPC | caller row lock後の再認可でdisabled callerを拒否 |
| 最後のmanagerを同時変更 | quorumを0にしない。通常suiteでの完全競合再現は未実装 |
安定点5173876の検証記録
今回のRegressionではHosted環境にtest外のactive admin_managerが2人存在したため、『最後のmanager』を作る前提が成立せずADMIN-005/006は安全にSKIPされました。runner集計はskipを分母から除外し、実行された280 caseがPASSです。環境依存で危険な操作を無理に行わない設計もtest harnessの重要な性質です。
| 検査 | 安定点固定時の記録 | このDocs作業での確認 |
|---|---|---|
| Regression | 280 / 280 PASS | 2026-08-23に再実行し、DB/RPC 259/259 + API 21/21 = 280/280 PASS |
| Browser E2E | 11 / 11 PASSを2回連続 | 2026-08-23に1回再実行し、11 / 11 PASS(1.7分) |
| TypeScript / Lint / Build | PASS | 専用worktreeで再実行しPASS |
| Hosted Migration | 全適用済み | Migrationをread onlyで参照。DocsからDB変更しない |
実際のMinibankでは
安定点の新suiteとしてtransferOtp、transferBeneficiaries、actorSeparationがDB/RPC runnerへ組み込まれ、privilege matrixにはrequest_transfer、confirm_transfer_otp、reissue_transfer_otp、admin_list_transfer_requestsが追加されています。お気に入りsuiteはowner分離、自己口座、重複、非owner更新・削除、last_used_at、audit、table ACLを確認します。
体験学習の続き · LEARNING LOOP
ここまで読めましたか?続けて確認してみましょう
テストの役割を読んだところで、教材内の参照元で実際のテスト記録を確認し、自分の言葉で説明してみます。
- 02仕組みを確かめる — CONCEPT CHECK
画面操作なしで、仕組みを確かめる
この仕組みは、Mini Bankの画面には現れません。次のEvidenceとCode Trail、教材本文だけで理解を進めます。
- 03証拠を確認する — EVIDENCE
何を見たか、1つずつ確かめる
Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。
画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。
- 04コードの処理経路を追う — CODE TRAIL
コードを書く必要はありません
ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。
基本の順序は、画面 → API Route(依頼を受ける窓口)→ TransferService(処理をつなぐサービス)→ DB / RPC(記録の保存場所と、その処理の呼び出し)です。まず最初の2つを見て、続くサービスは「もっと詳しく」で確認します。
教材の参照元を確認する — Source Trail ↓DBとAPIの回帰テストをまとめて実行するrunner。どの順序でテストが実行されるかを探します。
scripts/regression/run-regression.mjsRegression test(変更後も約束が保たれるかの確認)。#128以前のデモOTPの誤入力・再発行・期限切れなどを検査する実装を確認します。v7のTOTP認可は対象外です。
scripts/regression/suites/transferOtp.mjs - 05自分の言葉で説明する — EXPLAIN
LEVEL 1 — やさしい説明(まずここだけ)
なぜ「正常に動く」ことの確認だけでなく、失敗や攻撃を試すテストが必要なのでしょうか? 短い2文を目安に説明してください。
日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。
ヒント1を見る
「正常系」と「異常系」という2つの言葉を使って書いてみましょう。
ヒント2を見る
テストが実際に見つけたバグの例を、1つ挙げてみましょう。
もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)
DB regression test、concurrency test、failure recovery test、状態機械testの4つを使って、テストが守っている境界を説明してみましょう。
挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。
説明の確認ポイント — Checkpoint- 正常系だけでなく異常系や同時実行も確認する必要があると説明できる
- テストが実際にバグを発見した実績があると説明できる
理解を確かめる — CHECK YOUR UNDERSTANDING
4問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1同じ送金を順番に2回呼べばconcurrency testですか?
違います。別connectionから処理時間を重ね、同じ古い状態を読もうとする状況を作る必要があります。
Q2E2Eが通ればDB権限も安全ですか?
断定できません。直接RPC、GRANT、RLSなどはDBレベルの負のテストが必要です。
Q3Hosted環境のテストで注意することは?
実データを書き換える可能性があります。専用環境・専用データ・後片付け・実行権限を明確にします。
Q4280/280 PASSなら、すべての競合と負荷も安全ですか?
断定できません。合格は実装済み280 caseの結果であり、広範なmulti-session競合、負荷、耐久、災害復旧には別のtestが必要です。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
教材の参照元 — SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
- 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/run.mjsDB/RPC suite runner - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/run-api.mjsAPI suite runner - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/run-regression.mjsDBとAPIの回帰runner - 過去の実装 — HISTORICAL IMPLEMENTATION
scripts/regression/suites/transferOtp.mjs#128以前のデモOTP前提の回帰(OTP lifecycle・owner・冪等性・error境界)。v7のTOTP認可はscripts/mfa/intent-db.test.mjsが対象 - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/suites/transferBeneficiaries.mjs振込先owner・権限・audit - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/suites/privilege.mjsfunction ACL・table privilege・RLS・schema - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/suites/actorSeparation.mjscustomer/admin兼務の双方向guard - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170002_drop_old_execute_transfer_overload.sqlCase 1の原因と修正 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170003_fix_confirm_and_reissue_transfer_otp_bugs.sqlCase 2の原因と修正 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170004_sanitize_confirm_transfer_otp_failure_reason.sqlCase 3の原因と修正 - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/adhoc/verify-admin-toctou-fix.mjs限定的なmulti-session検証 - 現在の実装 — CURRENT IMPLEMENTATION
e2e/full-journey.spec.tsPlaywright E2E