Part 3 Webアプリケーション開発

第13章 テストとコード品質

第9章では、支援ステータス機能の受け入れ条件を書いた。 第10章では、支援ステータスを保存するデータ構造を考えた。 第11章では、API契約、入力検証、認可、エラーを設計した。 第12章では、APIを画面状態とアクセシビリティ確認へ変換した。

第13章では、それらをテストとコード品質へつなげる。 テストは、開発の最後に安心するための儀式ではない。 何を守りたいのかを明確にし、変更後もそのふるまいが保たれていることを説明するための道具である。

この章で扱うのは、QA職向けの網羅的なテスト分析ではない。 プロダクトエンジニアが自分の変更について、何を確認し、何を自動化し、何を手動確認に残し、どのリスクをまだ残しているのかを説明できるようになることを目標にする。 題材は引き続き、メンターが担当受講者の支援ステータスを確認し、必要に応じて更新する機能である。

この章でできるようになること

この章を終えると、次のことを自分の言葉で説明し、課題の成果物へ落とせるようになる。

  • 受け入れ条件、API契約、画面状態、確認ログからテスト観点を取り出せる。
  • テスト観点、テストケース、テストコード、手動確認ログの違いを説明できる。
  • 単体テスト、APIテスト、画面確認、E2E、手動確認を、守りたいリスクに合わせて選べる。
  • 前提、操作、期待結果、テストデータ、確認方法をそろえたテストケースを書ける。
  • TDDのRed、Green、Refactorを、小さなロジックで試せる。
  • fixturefactorymockstubを、何を置き換えたか説明しながら使える。
  • カバレッジやテスト数を目的にせず、未確認のリスクを見つける材料として扱える。
  • flaky testやテスト順序依存など、不安定なテストの原因を説明できる。
  • リファクタリングと仕様変更を分け、変えていないふるまいをテストと記録で示せる。
  • quality-review.md に、実行した確認、未確認事項、残したリスク、レビューで見てほしい点を書ける。

テストは、変更してよい範囲を示す地図である

テストがないコードでは、変更するときに不安が残る。 この条件を直したら、別の条件が壊れるかもしれない。 入力検証を追加したら、画面の保存処理が動かなくなるかもしれない。 権限チェックを強めたら、本来更新できるメンターまで拒否してしまうかもしれない。

テストは、この不安をゼロにするものではない。 しかし、どのふるまいを守っているかを明らかにする。 「担当メンターは更新できる」「担当外メンターは更新できない」「不正なstatusは保存されない」というテストがあれば、その範囲については変更後も確認できる。

この意味で、テストはコードの外側にある説明でもある。 実装の中身を知らないレビュアーでも、テスト名、前提、操作、期待結果を読めば、変更が何を守ろうとしているかを理解できる。 良いテストは、失敗したときに「何が壊れたか」を教える。 悪いテストは、失敗しても「何かが壊れたらしい」しか分からない。

テスト戦略は、先にリスクを選ぶ作業である

テスト戦略という言葉は大げさに聞こえるかもしれない。 テスト戦略は、何をどの方法で確認するかを決める短い方針、と考えればよい。 すべてを確認することはできない。 だから、壊れると困るふるまいと、起きやすい失敗を先に選ぶ。

支援ステータス機能では、少なくとも次のリスクがある。

  • 担当メンターが更新できない。
  • 担当外メンターが更新できてしまう。
  • 不正な status が保存される。
  • 保存後に一覧や画面表示へ反映されない。
  • 保存失敗時に、利用者が何をすればよいか分からない。
  • 保存中に二重送信できる。
  • 0件表示や読み込み失敗を、正常な一覧と区別できない。
  • 既存の一覧表示、絞り込み、権限チェックを壊す。

この時点では、まだテストコードを書かない。 まずリスクを表にして、影響と確認方法を選ぶ。

守りたいふるまい 壊れたときの影響 確認方法の候補 優先度
担当メンターだけが更新できる 業務が止まる、または権限外更新が起きる APIテスト、権限ロジックの単体テスト
不正な status を保存しない DBに想定外の値が入り、画面や集計が壊れる 単体テスト、APIテスト、DB制約の確認
保存結果が一覧へ反映される 利用者が保存できたか判断できない APIテスト、画面確認、必要ならE2E
保存失敗時に再試行できる 入力内容を失う、利用者が次の行動を取れない 画面確認、エラー変換の単体テスト
0件、読み込み失敗、保存中を区別する 正常なのか失敗なのか分からない 画面確認、UI stateの確認
既存の一覧表示や絞り込みを壊さない 既存利用者の導線が壊れる 回帰確認、既存テスト

テストの数から始めない。 「10件テストを書く」ではなく、「担当外更新を防ぐ」「不正値を拒否する」「保存失敗を利用者へ伝える」のように、守るふるまいから始める。 テスト数やカバレッジは参考になるが、それだけでは何を守れているかは分からない。

ここで、言葉を分けておく。

  • テスト観点:何を確認したいか。例は「担当外メンターの更新を拒否する」。
  • テストケース:前提、操作、期待結果を持つ具体的な確認項目。
  • テストコード:テストケースを自動実行できる形にしたもの。
  • 手動確認ログ:人が画面やブラウザ開発者ツールで確認した事実の記録。

この区別があると、まだ自動化していない確認も説明できる。 「テストがない」ではなく、「観点はあるが、今回は手動確認に残している」と言えるようになる。

受け入れ条件は、最初のテスト観点になる

第9章で書いた受け入れ条件は、テスト観点の出発点になる。 たとえば、次の受け入れ条件があるとする。

Given メンターが担当受講者の進捗一覧を開いている
When 支援ステータスを「要支援」に変更する
Then その受講者の支援ステータスが「要支援」として保存される

この一文から、いくつかの観点が取り出せる。 まず正常系として、担当メンターは担当受講者の支援ステータスを変更できる。 保存結果はresponseだけでなく、再取得した一覧やDB上の値にも反映される必要がある。 画面では、保存中と保存成功が分かる必要がある。

さらに、この受け入れ条件の裏側には、失敗時の観点がある。 担当外メンターは変更できない。 存在しない受講者は更新できない。 不正な支援ステータスは保存されない。 保存に失敗したら、利用者にエラーが表示され、再試行できる状態が残る。

受け入れ条件は、成功シナリオだけを確認するためのものではない。 成功するために満たすべき前提を読むと、失敗時に確認すべきことも見えてくる。

API契約、画面状態、確認ログを材料にする

第13章でゼロからテスト観点を考える必要はない。 すでに作った成果物を読み直す。

第11章の api-contract.md には、endpoint、request、response、error responseがある。 validation-and-authorization.md には、誰が何をしてよいか、不正な入力をどう拒否するかがある。 api-check-log.md には、curlなどで確認したrequestとresponseの記録がある。

第12章の ui-state-model.md には、loading、empty、error、saving、savedのような画面状態がある。 frontend-check-log.md には、Network、Console、Elements、キーボード操作、フォーカス、エラー表示の確認結果がある。

これらを、テスト観点へ変換する。

api-contract.md
  -> PATCHで更新できるか
  -> 403, 404, 400または422を期待どおり返すか

validation-and-authorization.md
  -> 担当メンターだけが更新できるか
  -> 不正なstatusを拒否できるか

ui-state-model.md
  -> loading, empty, error, saving, savedを表示できるか
  -> 保存中に二重送信できないか

frontend-check-log.md
  -> Networkで期待したrequestが出ているか
  -> Consoleに重大なエラーがないか
  -> キーボード操作で主要導線を進めるか

テストは、設計と別の作業ではない。 前章までに決めた言葉、状態、API、エラー、権限を、変更後も守るための確認へ変える作業である。

既存テストは、最初に読む仕様である

新しいテストを書く前に、既存テストを読む。 既存テストには、そのプロジェクトで大事にしているふるまい、テストデータの作り方、実行コマンド、失敗時の見方が表れている。

learning-log-sample では、package.jsontest script が node --test になっている。 これはNode.jsの組み込みテストランナーで、test/*.test.js のようなテストファイルを実行する構成である。 このサンプルでは、主に次の2種類のテストを見る。

ファイル 見ていること 読むときのポイント
test/store.test.js データ取得、支援ステータス絞り込み、更新、担当外更新の拒否、不正なstatusの拒否 DBやHTTPより内側の業務ロジックを小さく確認している
test/server.test.js HTTP requestを送って、一覧取得やPATCH更新のresponseを見る API契約と実際のrequest/responseが合うかを確認している

既存テストで resetData() のような初期化処理が使われているなら、各テストを独立させる意図がある。 あるテストが前のテストで更新したデータに依存すると、実行順序によって通ったり落ちたりする。 テストは、できるだけ「単独で実行しても同じ結果になる」状態にする。

サンプルアプリは研修用に小さくしているため、本物の認証基盤の代わりに x-mentor-id headerを使う。 また、不正な status400 で返す実装になっている場合がある。 第11章で 422 を採用した契約を書いた場合は、自分の契約に合わせて期待値を書く。 大切なのは、教材の例、スターターの実装、自分の api-contract.md のどれを正として確認しているのかを混ぜないことである。

テストの種類は、役割で選ぶ

テストには種類がある。 ただし、種類の名前を暗記するだけでは役に立たない。 どのリスクをどこで確認するのが自然かを考える。

種類 主な役割 支援ステータス機能での例 注意点
単体テスト 小さな関数やロジックを速く確認する status の許可値判定、error codeから画面文言への変換 DB、HTTP、画面のつながりは見えにくい
統合テスト 複数の部品を組み合わせて確認する use caseとrepositoryを組み合わせて、保存結果を見る どこまで本物を使うかを決める
APIテスト HTTP request、response、必要なら保存結果を見る PATCH更新、403、404、400/422、response body 認証の代替手段やテストデータの前提を書く
画面確認 利用者が見る状態と操作を確認する loading、empty、error、saving、saved、フォーカス 人の判断が必要な文言や操作感も見る
E2E ブラウザ操作から複数層をまたいで確認する 一覧を開き、ステータスを変更し、保存後の反映を見る 遅くなりやすく、失敗原因が広い
手動確認 自動化しにくい観点を事実として残す Network、Console、キーボード操作、アクセシビリティの目視確認 実行日、環境、結果、未確認事項を書く

単体テストは、小さな関数やロジックを速く確認する。 画面全体やDBを動かさなくても確認できる判断は、単体テストに向いている。

APIテストは、HTTP requestを送り、responseと保存結果を確認する。 担当メンターが更新できること、担当外メンターが403になること、不正な status が400または422になること、存在しない受講者が404になることは、APIテストで確認しやすい。 API契約と実装のずれを見つける役割がある。

画面確認やE2Eは、利用者に近い操作から流れを確認する。 一覧を開く、支援ステータスを選ぶ、保存ボタンを押す、保存中表示を見る、成功または失敗を確認する。 NetworkやConsoleも合わせて見ると、画面とAPIの接続まで説明できる。

手動確認に残すものもある。 文言が利用者に分かりやすいか、フォーカスが見えるか、キーボード操作が自然か、エラー表示が次の行動を示しているかは、人が画面を見て判断する必要がある。 すべてを自動化することが目的ではない。 確認したいリスクに合う方法を選ぶことが目的である。

テストピラミッドは、速さと範囲の配分を考える図である

テストピラミッドは、単体テスト、統合テスト、E2Eテストを上下に並べた有名な図である。 しかし、図の形を暗記しても、良いテストは書けない。 大切なのは、速さ、範囲、信頼性の違いを理解することである。

小さなテストは速い。 原因も追いやすい。 ただし、実際の画面やAPI接続の問題は見えにくい。

大きなテストは、利用者に近い流れを見られる。 ただし、実行が遅く、失敗原因も広くなりやすい。 すべてをE2Eだけで確認すると、変更のたびに時間がかかり、失敗したときの調査も難しくなる。

支援ステータス機能なら、次のように分けられる。

  • status の許可値判定は単体テストで速く見る。
  • PATCH APIの認可と入力エラーはAPIテストで見る。
  • 保存中表示、保存失敗表示、キーボード操作は画面確認で見る。
  • 代表的な更新の流れは、必要ならE2Eで見る。

どれか一種類が正しいのではない。 同じ機能を、リスクごとに違う高さから確認する。

カバレッジは、品質の点数ではなく手がかりである

カバレッジは、テスト実行時にどの行や分岐が通ったかを示す指標である。 便利な指標だが、品質の点数ではない。 ある行が実行されたことと、その行の期待結果を確かめたことは違う。

たとえば、不正な status を送ったときの処理行がカバレッジ上は実行されていても、response status、error code、DB値が変わらないことをassertしていなければ、重要なふるまいを守れていない可能性がある。 逆に、カバレッジが低い場所は、未確認の分岐や例外処理が残っている手がかりになる。

カバレッジを見るときは、次のように使う。

  • 重要な業務ルールの分岐が、テストで一度も通っていない箇所を見つける。
  • 失敗時の処理、権限エラー、境界値の確認漏れを探す。
  • 数値を上げるためだけの、期待結果が薄いテストを増やさない。
  • 「何%だから安全」ではなく、「この重要なリスクはこのテストで確認した」と説明する。

カバレッジは、レビューで会話を始める材料である。 ゴールは数字ではなく、変更によって壊したくないふるまいを説明できることである。

テストケースは、前提、操作、期待結果をそろえる

テスト観点は、まだ抽象的である。 実行できる確認にするには、テストケースへ変換する。

良いテストケースには、少なくとも前提、操作、期待結果がある。 誰が、どの対象に、何をして、何が起きるべきかを書く。 さらに、テストデータと確認方法があると、別の人が読んでも再現しやすい。

# Test Case

名前:
- 担当外メンターが担当外受講者の支援ステータスを変更すると403になる

種類:
- API

前提:
- `assignedMentor``otherMentor` がいる。
- `assignedLearner``assignedMentor` の担当である。
- `otherMentor``assignedLearner` の担当ではない。

操作:
- `otherMentor` として `assignedLearner``status``needs_support` に変更する。

期待結果:
- response status が403になる。
- DB上の `status` は変更されない。
- response body に権限エラーを示すcodeが入る。

確認方法:
- APIテストでresponseとDB上の値を確認する。

この形式で書くと、実装者、レビュアー、メンターが同じものを見られる。 テストコードを書く前に、何を確認するかの合意が作れる。

同じ形式で、支援ステータス機能の代表ケースを並べると次のようになる。

名前 種類 前提 操作 期待結果
担当メンターは更新できる API m-001l-102 を担当している needs_support へPATCHする 200になり、responseと保存値が更新後の値になる
担当外メンターは更新できない API m-002l-102 を担当していない needs_support へPATCHする 403になり、保存値は変わらない
不正なstatusは保存されない 単体/API 許可値は none, needs_support, in_progress, resolved urgent を送る 契約に合わせて400または422になり、保存値は変わらない
存在しない受講者は更新できない API 対象IDが存在しない 存在しないIDへPATCHする 404になる
保存中は二重送信できない 画面確認 保存処理が進行中 保存ボタンを連続で押す 追加requestが出ない、またはボタンが無効になる
保存失敗時に再試行できる 画面確認/API APIがエラーを返す 保存する エラー表示が出て、入力内容と再試行手段が残る
0件状態を表示できる 画面確認 担当受講者が0件 一覧を開く 空状態が表示され、エラーと誤解しない
保存後に一覧へ反映される 画面確認/E2E 一覧に対象受講者が表示されている 更新して保存する 行の表示が更新され、再読み込み後も残る

正常系だけでなく、境界と失敗を先に見る

初心者が最初に確認しやすいのは、うまくいく流れである。 しかし、実務で問題になりやすいのは、境界と失敗である。

境界値とは、許される値と拒否される値の境目にある入力である。 支援ステータスなら、許可値は noneneeds_supportin_progressresolved である。 空文字、nullurgent、大小文字違い、余分な空白を含む値はどう扱うのか。 支援メモに文字数制限があるなら、0文字、上限ちょうど、上限超過をどう扱うのか。

失敗時のケースも先に並べる。

  • 未ログインなら401になる。
  • 担当外メンターなら403になる。
  • 対象の受講者が存在しなければ404になる。
  • 不正な status なら、契約に合わせて400または422になる。
  • 一覧取得に失敗したら、画面に再試行可能なエラーを出す。
  • 保存に失敗したら、入力内容を失わず、再試行できる。
  • 保存中は二重送信できない。

失敗時を先に考えると、実装の条件分岐が自然になる。 また、画面の文言やAPI error codeも設計とずれにくくなる。

TDDは、小さな期待を先に言葉にする技法である

TDDは Test-Driven Development の略である。 日本語ではテスト駆動開発と訳される。 TDDは、すべての設計をテストだけで進める高度な手法としてではなく、小さな期待を先にテストにする練習として扱う。

TDDの基本は、Red、Green、Refactorである。 Redでは、期待するふるまいをテストに書き、まず失敗を確認する。 Greenでは、そのテストを通すための最小限の実装を書く。 Refactorでは、ふるまいを変えずに名前、重複、構造を整える。

支援ステータス機能なら、不正な status を拒否するロジックが小さな題材になる。

const allowedSupportStatuses = ["none", "needs_support", "in_progress", "resolved"] as const;

type SupportStatus = (typeof allowedSupportStatuses)[number];

export function isSupportStatus(value: string): value is SupportStatus {
  return allowedSupportStatuses.includes(value as SupportStatus);
}

この実装をいきなり書くのではなく、先に次のような期待を書く。

Red:
- `needs_support` は有効な支援ステータスとして扱う。
- `urgent` は不正な支援ステータスとして拒否する。

Green:
- 許可値の配列を作り、含まれるかを判定する。

Refactor:
- 許可値の配列名を、意味が分かる `allowedSupportStatuses` にする。
- API側と画面側で同じ定義を使えるか確認する。

Redを飛ばさない。 失敗を確認しないままテストを書くと、そのテストが本当に期待した失敗を検出できるか分からない。 テストが最初から通る場合、実装が正しいのか、テストが何も検出していないのかを区別しにくい。

Redでは、ただ赤くなればよいわけではない。 期待した理由で失敗しているかを見る。 たとえば「urgent を拒否するテスト」を書いたのに、関数名のimportミスで落ちているなら、それはまだ期待したRedではない。 TDDの記録には、失敗したコマンドと、失敗理由を短く残す。

## Red

実行:
- npm test

結果:
- `urgent is rejected as support status` が失敗。
- 失敗理由は `actual: true, expected: false`

分かったこと:
- 現在の判定は任意の文字列を許している。

Greenでは、最小限の実装で通す。 この段階で大きな設計変更を同時に進めると、何でテストが通ったのか分かりにくくなる。 Refactorでは、テストが通っている状態を保ちながら、名前、重複、責務を整える。

TDDが向く場面と向かない場面を分ける

TDDは便利だが、すべての作業に同じ強さで使う必要はない。 小さなロジック、入力検証、値変換、エラー変換、バグ修正の再現テストには向いている。 期待するふるまいを短く書けるからである。

一方で、画面全体の体験がまだ揺れているときや、利用者と文言を調整している段階では、先に小さな試作や手動確認が必要になることもある。 その場合でも、固まったロジックや重要なバグは、後から自動テストへ切り出せる。

TDDを使うかどうかは、信仰の問題ではない。 今扱っている変更を、小さな期待として書けるか。 失敗を確認し、最小実装で通し、ふるまいを変えずに整理できるか。 この問いで判断する。

テスト名は、失敗したときの説明になる

テスト名は、将来の読者への説明である。 testUpdateshouldWork のような名前では、何を守っているのか分からない。 失敗したときにも、何を調べればよいか分からない。

支援ステータス機能なら、次のように書くほうがよい。

  • 担当メンターは担当受講者の支援ステータスを要支援に変更できる。
  • 担当外メンターが更新しようとすると403になり、保存値は変わらない。
  • 不正なstatusを送ると契約に合わせて400または422になり、エラーcodeが返る。
  • 保存失敗時は入力内容を保持し、再試行できる。

テスト名には、前提、操作、期待結果のうち、読者が理解するために必要な情報を入れる。 長すぎる名前を恐れる必要はない。 短くても意味が薄い名前より、多少長くてもふるまいが読める名前のほうがよい。

テストデータは、前提を隠しすぎない

テストデータが読みにくいと、テストの意味も読みにくくなる。 user1user2itemA のような名前では、誰が担当メンターで、誰が担当外メンターなのか分からない。

支援ステータス機能では、役割が分かる名前を使う。

  • assignedMentor:担当メンター。
  • otherMentor:担当外メンター。
  • assignedLearner:担当受講者。
  • unassignedLearner:担当外受講者。

fixtureは、テストで使う前提データを再利用しやすく用意する仕組みである。 factoryは、条件に合わせてテストデータを作る仕組みである。 どちらも便利だが、前提を隠しすぎると、テストを読んだ人が何を確認しているのか分からなくなる。

方式 向いている場面 注意点
fixture 毎回同じ基準データで確認したい 巨大になると、どのデータがテストに必要か分からなくなる
factory テストごとに少し違う条件を作りたい 初期値が多すぎると、重要な前提が隠れる
直接記述 前提を明示したい小さなテスト 同じデータを何度も書くと保守しづらい

テストデータでは、独立性も重要である。 あるテストが更新した値を、次のテストが前提として使うと、実行順序を変えたときに壊れる。 resetData() のような初期化、テストごとの一時DB、トランザクションのrollbackなどを使い、各テストが単独で実行できる状態を作る。

テストデータは、短さよりも意味を優先する。 特に権限や状態遷移のテストでは、誰と誰がどう関係しているかが重要である。 データ名だけで前提が読めるようにする。

mockとstubは、何を置き換えたかを書く

mockやstubは、本物の外部部品の代わりに使うテスト用の部品である。 時刻、メール送信、外部API、ランダムな値、重い処理などは、本物を毎回動かすと不安定になったり、時間がかかったりする。 そのため、テストでは代替部品を使うことがある。

stubは、決まった応答を返す代替部品として考えると分かりやすい。 たとえば、外部通知サービスの代わりに「成功した」という応答だけを返す。

mockは、代替部品がどう呼ばれたかを確認する用途で使われることが多い。 たとえば、保存成功時に通知処理が1回呼ばれたかを確認する。 mock、stub、fake、spyのような代替部品をまとめてテストダブルと呼ぶこともある。 用語の細かい違いよりも、何を本物から置き換えたかを説明できることが大切である。

ただし、置き換えすぎると、実際の接続の問題を見落とす。 APIのrequest形式、認可、DB保存、画面のNetwork確認までmockで済ませると、実物を動かしたときに壊れる可能性が残る。 mockやstubを使ったら、何を置き換えたのか、何は実物で確認したのかを test-cases.mdquality-review.md に書く。

置き換える候補として自然なのは、外部API、メール送信、時刻、乱数、重い処理である。 一方で、自分が確認したい業務ルールそのものをmockにしてしまうと、テストの意味が薄くなる。 「担当外メンターは更新できない」を確認したいのに、認可判定を常に成功するstubに置き換えると、そのリスクは確認できない。

flaky testは、信頼を削る不安定なテストである

flaky testは、同じコードなのに通ったり落ちたりする不安定なテストである。 一度でも頻繁に起きると、チームはテスト失敗を真剣に見なくなる。 その結果、本当の不具合を見逃しやすくなる。

flaky testの原因には、次のようなものがある。

  • テスト同士が同じデータを共有し、実行順序に依存している。
  • 現在時刻、タイムゾーン、乱数に依存している。
  • setTimeout の待ち時間だけで非同期処理を待っている。
  • 外部APIやネットワークに直接依存している。
  • 画面テストで、要素が表示される前に操作している。
  • 並列実行時に同じport、同じファイル、同じDBを取り合っている。

対策は、原因に合わせて考える。 テストデータを初期化する。 時刻や乱数を固定する。 外部APIはstubにする。 画面では、要素が表示されたことを待ってから操作する。 portや一時ファイルはテストごとに分ける。

不安定なテストは、放置せず quality-review.md に書く。 「時々落ちるが再実行で通った」とだけ書くのではなく、どのテストが、どの条件で、何回中何回失敗したかを記録する。

回帰確認は、直した場所以外を見るためにある

回帰とは、以前は動いていたふるまいが、変更によって壊れることである。 回帰確認は、直した場所だけでなく、周辺の既存機能が壊れていないかを見る確認である。

支援ステータス更新を直した場合、見るべきものは更新処理だけではない。 担当受講者一覧が表示されるか。 支援ステータスで絞り込めるか。 保存後に一覧へ反映されるか。 担当外メンターの権限拒否が壊れていないか。 不正な status の拒否が壊れていないか。 画面の保存中表示やエラー表示が壊れていないか。

バグ修正では、再現テストを残すと効果が高い。 一度起きたバグは、同じような変更で戻ることがある。 再現テストは、「このバグは戻さない」という印になる。

既存ふるまいが曖昧なら、先に固定する

リファクタリングしたいコードでは、既存のふるまいが十分に分かっていないことがある。 その状態で内部構造を変えると、意図せず仕様変更を混ぜやすい。

既存ふるまいを固定するためのテストを、characterization testと呼ぶことがある。 これは「理想の仕様」を表すテストではなく、「今のコードはこの入力に対してこう動いている」と記録するテストである。 古いコードを整理するとき、まず現在のふるまいを捕まえ、その後で構造を変える。

支援ステータスAPIなら、リファクタリング前に次を確認する。

  • 担当メンターの更新成功時のstatus codeとresponse body。
  • 担当外メンターの更新拒否時のstatus codeとerror code。
  • 不正な status のstatus codeとerror code。
  • 保存値が変わるケースと変わらないケース。
  • 既存の一覧取得、絞り込み、0件表示が変わらないこと。

もし現在のふるまいが明らかに不具合なら、そのまま固定し続ける必要はない。 その場合は、先に「現在はこう動くが、受け入れ条件ではこう直す」と分けて書く。 大切なのは、知らないうちに変えないことである。

リファクタリングは、ふるまいを変えない変更である

リファクタリングは、外から見えるふるまいを変えずに、内部構造を整理する変更である。 名前を分かりやすくする。 重複を減らす。 長い関数を分ける。 入力検証、認可、DB更新、response作成の責務を分ける。 テストしづらい依存を切り出す。

支援ステータスAPIで、handlerの中にすべてが混ざっているとする。

handler:
  requestを読む
  statusを検証する
  ログインユーザーを確認する
  担当関係を確認する
  DBを更新する
  responseを作る

この状態でも動くかもしれない。 しかし、入力検証だけをテストしたいとき、権限チェックだけを読みたいとき、エラーresponseだけを変えたいときに扱いづらい。 責務を分けると、読みやすさとテストしやすさが上がる。

handler:
  HTTP requestとresponseを扱う

use case:
  支援ステータス更新の業務ルールを扱う

repository:
  DB読み書きを扱う

presenter:
  error codeやresponse bodyを組み立てる

ただし、分ければ分けるほど良いわけではない。 小さすぎる分割は、読む場所を増やす。 リファクタリングの目的は、構造を格好よくすることではない。 次の変更を安全にし、テストしやすくし、レビューしやすくすることである。

リファクタリングは、次の順番で進めると説明しやすい。

  1. 変えないふるまいを書く。
  2. そのふるまいを守る既存テスト、または追加する確認を決める。
  3. 小さい単位で構造を変える。
  4. 変更ごとにテストを実行する。
  5. 差分を読み、名前、責務、重複、条件分岐を確認する。
  6. 仕様変更が混ざった場合は、PR本文や refactoring-note.md で明記する。

仕様変更とリファクタリングを混ぜすぎない

仕様変更とリファクタリングを同じ差分に混ぜすぎると、レビューが難しくなる。 どこでふるまいが変わったのか、どこは整理だけなのかが見えなくなるからである。

たとえば、支援ステータスに新しい値を追加しながら、handlerの責務分割も行い、エラー文言も変え、画面の保存処理も変える。 このような差分は、何を確認すればよいかが広がりすぎる。 レビューする人は、仕様変更の妥当性と、リファクタリング後のふるまい維持を同時に見なければならない。

可能なら、ふるまいを変える変更と、ふるまいを変えない整理を分ける。 分けられない場合でも、PR本文や refactoring-note.md に明記する。

  • 変えたふるまいは何か。
  • 変えていないふるまいは何か。
  • どのテストで変えていないことを確認したか。
  • どの範囲は未確認か。

リファクタリングでは、「何を変えないか」を最初に書く。 これがないと、整理のつもりで仕様変更を混ぜても気づきにくい。

内部品質は、未来の変更速度に効く

内部品質は、利用者から直接は見えにくい。 しかし、次の変更速度に大きく影響する。

名前が曖昧なコードは、読むたびに推測が必要になる。 責務が混ざった関数は、少し変えるだけで広い影響が出る。 重複したロジックは、一箇所だけ直して別の箇所が古いまま残る。 複雑な条件分岐は、境界値や例外ケースを見落としやすい。 テストしづらい構造は、変更のたびに手動確認へ依存しやすくなる。

コード品質は、好みの問題ではない。 読みやすさ、変更しやすさ、テストしやすさ、レビューしやすさは、プロダクトを継続して直すための実務上の品質である。

第13章では、次の観点で見る。

  • naming:名前から役割が分かるか。
  • responsibility:入力検証、認可、DB更新、response作成が混ざりすぎていないか。
  • duplication:同じ判定や変換が複数箇所に散っていないか。
  • complexity:条件分岐が読める大きさか。
  • testability:小さく確認できる単位があるか。

技術的負債は、残すなら記録する

技術的負債は、後で直す必要が出る設計や実装上の借りである。 負債という言葉は、悪いコードを責めるための言葉ではない。 時間の制約、研修の範囲、優先度の都合で、あえて残すこともある。

ただし、意図的に残すなら記録する。 何を残したのか。 なぜ今回直さないのか。 どのような影響があるのか。 いつ、または何が起きたら直すのか。 これらを書かずに放置すると、後から読む人には、意図した判断なのか見落としなのか分からない。

支援ステータス機能なら、たとえば次のような負債があり得る。

  • 画面確認はブラウザ1種類だけで、モバイル実機は未確認。
  • E2Eは未整備で、代表フローは手動確認に残している。
  • 支援メモの文字数制限はAPI側だけで、画面側の事前表示は未実装。
  • error codeから画面文言への変換がコンポーネント内に残っている。

負債は、隠すよりも書いたほうが扱いやすい。 書けば、次の改善候補になる。

quality-reviewは、確認済みの範囲を説明する文書である

quality-review.md は、品質を保証する宣言ではない。 自分の変更について、何を確認し、何を確認していないかを事実としてまとめる文書である。

書くべきことは、次のように分ける。

  • 変更概要。
  • 対応した受け入れ条件。
  • 実行した自動テスト。
  • 実行した手動確認。
  • コード品質の確認。
  • 確認していないこと。
  • 残したリスク、負債。
  • PR本文に書くこと。
  • レビューで重点的に見てほしいこと。

大切なのは、実行していない確認を「確認済み」と書かないことである。 「APIテストは実行済み。画面の保存失敗表示は手動確認済み。キーボード操作は未確認」のように書けば、レビュアーは次に何を見るべきか判断できる。

自動テストは、コマンドと結果を書く。 「テストした」だけでは、何を実行したのか分からない。

## 実行した自動テスト

| コマンド | 結果 | 確認したこと |
| --- | --- | --- |
| npm test | pass | storeとserverの既存テストが通る |
| npm test -- --test-name-pattern support | pass | 支援ステータス関連のテストが通る |

## 実行できなかった確認

| 確認 | 理由 | 残るリスク |
| --- | --- | --- |
| E2E | この章ではE2E環境を用意していない | 画面からAPIまでの代表フローは手動確認に残る |
| スクリーンリーダー | 手元環境で未実施 | 読み上げ順の問題は残る |

プロジェクトによっては、テスト名の絞り込み方法が違う。 使えないオプションを無理に書かず、そのrepositoryで実際に実行したコマンドを書く。

品質レビューは、自分を守るためだけの文書ではない。 チームが変更を受け入れるかどうかを判断する材料である。 確認済みの範囲と未確認の範囲が分かるほど、レビューは具体的になる。

AIは、テスト観点を広げる補助に使える

AIは、テストコードを書く道具としてだけ使うものではない。 テスト観点の洗い出し、テストケース名の改善、境界値の候補、リファクタリング候補、品質レビュー観点のチェックに使える。

依頼するときは、前提を具体的に渡す。

支援ステータス機能のテスト観点を整理したいです。
実データ、秘密情報、個人情報は含めていません。

前提:
- メンターは担当受講者だけを変更できる
- statusは none, needs_support, in_progress, resolved
- APIは 401, 403, 404, 400または422 を返す
- 画面には loading, empty, error, saving, saved がある
- テスト設計はプロダクトエンジニア向けの基本範囲に留める

出してほしいこと:
- 単体テストの候補
- APIテストの候補
- 画面確認の候補
- 手動確認に残すべきこと
- 見落としやすいケース
- 優先度

AIの出力は、候補である。 採用する前に、受け入れ条件、API契約、画面状態と照合する。 テストコード案が出たら、実行する。 失敗すべきときに失敗するか、通るべきときに通るか、実装詳細に寄りすぎていないかを見る。

AIが作ったテストでは、次の点を疑って読む。

  • 存在しない関数名、ファイル名、ライブラリを使っていないか。
  • 実装の内部構造だけを確認し、利用者に見えるふるまいを確認していないのではないか。
  • mockしすぎて、確認したい認可、保存、API契約まで置き換えていないか。
  • テストデータの前提が、自分のDB設計やstarterのデータと合っているか。
  • 400422idlearnerId など、自分のAPI契約と違う期待値を混ぜていないか。
  • 実行していないのに、AIの文章だけで「pass」と書いていないか。

不安な場合は、実装を一時的に壊す、または期待値を一時的に変えて、テストが失敗することを確認する。 これはテストが本当にそのふるまいを見ているかを確かめる簡単な方法である。 作業後は、意図的に壊した差分を必ず戻す。

AIに任せてはいけないこともある。 テストの重要度判断、実行していないテストの成功報告、既存テストの意図の確定、リファクタリング後にふるまいが変わっていないことの判断である。 これらは、開発者が文脈と実行結果を見て決める。

test-strategy.mdに書くこと

test-strategy.md は、テストの方針を短くまとめる文書である。 対象機能、守りたいふるまい、テスト観点、自動化するもの、手動確認に残すもの、今回対象外を書く。

# Test Strategy

## 対象機能

- メンターが担当受講者の支援ステータスを一覧で確認し、更新できる機能。

## 守りたいふるまい

- 担当メンターだけが更新できる。
- 不正なstatusは保存されない。
- 保存結果が一覧と再読み込み後の表示に反映される。
- 保存失敗時に、利用者が再試行できる。

## テスト観点

- **normal**:担当メンターの更新成功。
- **validation**:不正なstatus、支援メモの文字数上限。
- **authorization**:担当外メンターの403。
- **UI state**:loading、empty、error、saving、saved。
- **regression**:一覧表示、絞り込み、保存後の反映。

## 自動テストにするもの

- statusの許可値判定。
- PATCH APIの正常系、403、404、400/422。

## 手動確認に残すもの

- 保存中表示、保存失敗表示、文言、フォーカス、キーボード操作。

## 今回対象外

- 性能テスト、負荷テスト、CI/CD設定の詳細。

この文書は、テストコードの一覧ではない。 何を守るか、どの確認方法を選ぶかを説明するための方針である。

test-cases.mdに書くこと

test-cases.md では、観点を具体的な確認項目へ変換する。 前提、操作、期待結果、テストデータ、確認方法を書く。

最低限、次のケースを検討する。

  • 担当メンターは担当受講者の支援ステータスを変更できる。
  • 担当外メンターは変更できない。
  • 不正な status は保存されない。
  • 存在しない対象を指定したら404になる。
  • 一覧取得に失敗したらエラーが表示される。
  • 保存に失敗したらエラーが表示され、元の入力と再試行手段が残る。
  • 受講者が0件なら空状態が表示される。
  • 保存中は二重送信できない。
  • 保存後に一覧へ反映される。

ケースが多くなったら、優先度を付ける。 重要度が高く、壊れると業務に影響するものから自動化する。 文言や操作感のように人の判断が必要なものは、手動確認として残してよい。 ただし、手動確認に残した理由を書く。

tdd-log.mdに書くこと

tdd-log.md は、TDDの成功談を書く文書ではない。 どの小さなふるまいを選び、どの失敗から始め、どの最小実装で通し、何を整理したかを残す文書である。

書く順番は、Red、Green、Refactorである。

  • Red:書いたテストと失敗内容。
  • Green:最小限の実装と通ったテスト。
  • Refactor:改善した名前、重複、責務、変えていないふるまい。

題材は小さくてよい。 不正な status を拒否する。 API error codeから画面文言へ変換する。 保存中フラグを扱う。 バグ修正の再現テストを書く。

きれいな記録にする必要はない。 むしろ、最初に何が失敗し、そこから何を学んだかが見えるほうがよい。

refactoring-note.mdに書くこと

refactoring-note.md では、ふるまいを変えずに何を改善したかを書く。 最初に、変えないふるまいを書く。 これが、仕様変更とリファクタリングを分ける基準になる。

# Refactoring Note

## 対象

- 支援ステータス更新APIのhandler。

## 変えないふるまい

- 担当メンターは担当受講者の支援ステータスを変更できる。
- 担当外メンターの更新は403になる。
- 不正なstatusは契約に合わせて400または422になる。

## 見つけた問題

- handler内で入力検証、認可、DB更新、response作成が混ざっている。
- statusの許可値判定が画面側とAPI側で重複している。

## 改善方針

- 入力検証を関数に切り出す。
- 担当関係の確認をuse caseに寄せる。
- response作成を小さな関数に分ける。

## 実行したテスト、確認

- status許可値の単体テスト。
- PATCH APIの正常系、403、400/422。
- 保存成功と保存失敗の画面確認。

リファクタリングの説明では、「きれいにした」だけでは足りない。 どの問題があり、どう改善し、何でふるまい維持を確認したかを書く。

quality-review.mdに書くこと

quality-review.md は、PRに出す前のセルフレビューである。 差分を読み、実行したテスト、手動確認、未確認事項、コード品質上の懸念、残した負債を書く。

# Quality Review

## 変更概要

- 支援ステータス更新APIの入力検証と認可確認を整理した。
- 画面の保存失敗表示を追加した。

## 対応した受け入れ条件

- 担当メンターが担当受講者の支援ステータスを変更できる。
- 担当外メンターは変更できない。

## 実行した自動テスト

- status許可値の単体テスト。
- PATCH APIの正常系、403、400/422。

## 実行コマンド

| コマンド | 結果 | メモ |
| --- | --- | --- |
| npm test | pass | storeとserverの既存テストを確認 |

## 実行した手動確認

- 保存中表示。
- 保存成功表示。
- 保存失敗表示。
- NetworkとConsole確認。

## 確認していないこと

- スクリーンリーダーでの読み上げ。
- モバイル実機での確認。

## 残したリスク、負債

- E2Eは未整備で、代表フローは手動確認に残している。

この文書があると、PR本文が書きやすくなる。 また、レビューで見てほしい場所を具体化できる。

テストと品質で起きやすい誤解

  • テストをたくさん書けば品質が上がると考える。守りたいふるまいとリスクに結びつかないテストは、保守コストだけ増える。
  • カバレッジだけを目標にする。カバレッジは実行された行を示すが、期待したふるまいを確認できているとは限らない。
  • 正常系だけを書いて安心する。実務では、権限なし、不正入力、保存失敗、0件表示、回帰確認が重要になる。
  • すべてをE2Eで確認する。利用者に近い確認は重要だが、遅く壊れやすいテストだけになると開発速度が落ちる。
  • flaky testを再実行で通ればよいと考える。不安定な原因を放置すると、テスト失敗への信頼が落ちる。
  • 実装の関数名だけをテスト名にする。テスト名は、守りたいふるまいが読める名前にする。
  • fixtureやfactoryで前提を隠しすぎる。テストデータは、誰が何の役割か分かる名前にする。
  • mockやstubで置き換えた範囲を記録しない。何を本物で確認したかが分からなくなる。
  • リファクタリングで仕様変更を混ぜる。変えるふるまいと変えないふるまいを分けて説明する。
  • テストが通ったとだけ書き、何を実行したかを残さない。コマンド、対象、結果、未確認事項を書く。
  • AIが作ったテストを実行せずに採用する。AIの出力は候補であり、実行結果と差分で検証する。

テスト戦略と品質レビューで確認すること

この章では、test-strategy.mdtest-cases.mdtdd-log.mdrefactoring-note.mdquality-review.md を作る。

最初に、第9章の acceptance-criteria.md、第11章の api-contract.mdvalidation-and-authorization.mdapi-flow.mdapi-check-log.md、第12章の ui-state-model.mdfrontend-check-log.md を読み直す。 そこから、守りたいふるまい、テスト観点、確認方法を取り出す。

test-strategy.md には、対象機能、守りたいふるまい、正常系、入力不正、権限、UI state、回帰確認、自動化するもの、手動確認に残すものを書く。

test-cases.md には、前提、操作、期待結果、テストデータ、確認方法を書く。 403、404、400/422、保存失敗、0件表示、二重送信、保存後の反映を候補に入れる。

tdd-log.md には、小さなロジックを一つ選び、Red、Green、Refactorを記録する。 失敗するテスト、最小実装、整理した内容、変えていないふるまいを書く。

refactoring-note.md には、対象、変えないふるまい、見つけた問題、改善方針、実施した変更、実行した確認、残した負債を書く。

quality-review.md には、変更概要、対応した受け入れ条件、自動テスト、手動確認、実行コマンド、コード品質の確認、未確認事項、flaky testの有無、残したリスク、PR本文に書くこと、レビューで見てほしいことを書く。

成果物は、提出のためだけに作るものではない。 PR、レビュー、最終発表、Production Readiness Reviewで、自分の変更を説明する証拠になる。

テストとコード品質の章で持ち帰ること

第13章で身につけるべきことは、テストを量ではなく、守りたいふるまいとリスクから考えることである。 受け入れ条件、API契約、画面状態、確認ログは、テスト観点の材料になる。 テストの種類は、単体、API、画面確認、E2E、手動確認の役割で選ぶ。 TDDは、小さな期待を先に書き、失敗、最小実装、整理を短く回す技法である。 カバレッジは点数ではなく、未確認箇所を探す手がかりとして使う。 flaky testは再実行でごまかさず、原因と残るリスクを記録する。 リファクタリングは、ふるまいを変えずに内部構造を良くする作業であり、テストはその支えになる。

品質レビューでは、確認したことだけでなく、確認していないこと、残した負債、レビューで見てほしいことを書く。 実務で信頼されるのは、すべて確認済みと言い切る人ではない。 確認済みの範囲と未確認の範囲を、事実として説明できる人である。

セキュリティの基本の章へ

次章では、同じ支援ステータス機能をセキュリティの観点から見る。 テストで「期待どおり動くか」を確認しただけでは、安全に使えるとは限らない。 第14章では、認証、認可、入力、secret、依存関係を、自分の機能に結びつけて確認する。

参考資料

教材を検索