Part 2 開発の基本動作

第8章 既存コードを小さく読む

第5章では、手元でアプリを動かし、ログを残した。 第6章では、変更をGitの履歴とPull Requestでチームへ渡す方法を学んだ。 第7章では、画面の裏側にあるHTTP通信を観察した。 第8章では、この三つを組み合わせて、既存コードの不具合を小さく読む。

実務では、新しいコードを一から書く時間だけが開発ではない。 すでに動いているコードを読み、不具合を再現し、原因候補を絞り、壊さない範囲で直す時間も長い。 初学者がつまずくのは、コードが読めないからだけではない。 読む前に何を知りたいのかを決めず、広いコードベースへそのまま入ってしまうからである。

既存コードは、全部読む前に入口を探す。 画面で再現し、HTTP通信を見て、ログを読み、検索語を作り、関連しそうなファイルだけを追う。 この章で学ぶのは、正解の行を一発で当てる方法ではない。 調査範囲を小さくし、仮説を確認し、修正と確認結果をPRで説明する方法である。

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

この章のゴールは、勘で不具合の原因を当てることではない。 再現、観察、コード読解、仮説、最小修正、確認、PR説明を、あとから追える形で進められるようになることである。

  • 不具合を、再現手順、期待結果、実際の結果に分けて書ける。
  • Networkタブとcurlの観察から、画面側、API側、データ処理、ログのどこを見るかを絞れる。
  • URL、API path、field名、画面文言、エラーメッセージを検索語にできる。
  • 関連ファイルを、画面、API handler、ロジック、データ、テストに分けて読める。
  • 原因候補を複数出し、それぞれの確認方法と確認結果を書ける。
  • 原因に対応する最小修正を選び、リファクタリングや別仕様を混ぜすぎない判断ができる。
  • 修正後に、再現確認、回帰確認、既存テスト、差分確認を行い、PRで説明できる。
  • AIに原因候補や検索語を出させる場合でも、実行結果と差分で検証できる。

題材は、未提出者フィルタの不具合である

この章では、メンター向け進捗一覧の不具合を題材にする。 未提出者フィルタを選んでも、提出済みの受講者が一覧に残る、という状態である。

期待結果は、submittedAtnull の受講者だけが表示されることである。 submittedAt は提出日時を表すfieldであり、値が入っていなければ未提出、日時が入っていれば提出済みと考える。

実際の結果は、submittedAt に日時が入っている受講者も表示されることである。 つまり、未提出者フィルタが意図どおり働いていない。

サンプルアプリのデータでは、submittedAtnull の受講者と、日時が入っている受講者が混在している。 この混在があるから、フィルタの正しさを観察できる。 全員が未提出、または全員が提出済みのデータだけでは、条件が正しく働いているか判断しにくい。

ここで、いきなり条件式を書き換えてはいけない。 まず、どこで間違っているのかを観察する。 画面が条件を付けていないのか。 APIが filter=unsubmitted を読んでいないのか。 APIは読んでいるが、submittedAt の条件が逆なのか。 画面側でAPIの結果を上書きしているのか。 テストデータが想定と違うのか。

不具合修正の最初の仕事は、原因を決めつけることではない。 原因候補を、確認できる形へ分けることである。

直す前に、再現できる説明を作る

デバッグの最初の成果物は、修正コードではない。 再現ログである。 再現とは、同じ手順で同じ不具合を起こし、修正前後を比べられる状態にすることである。

再現ログには、少なくとも次を書く。

  • 対象の不具合。
  • 実行環境。
  • 再現手順。
  • 期待結果。
  • 実際の結果。
  • Networkタブで見たrequest URL、method、status。
  • responseの抜粋。
  • サーバーログ。
  • 観察した事実。
  • まだ分からないこと。

たとえば、次のように書ける。

再現手順: サンプルアプリを起動し、http://localhost:3000/mentors/progress を開く。 未提出者フィルタを選ぶ。

期待結果: submittedAtnull の受講者だけが表示される。

実際の結果: submittedAt に日時が入っている受講者も表示される。

Network: GET /api/mentor/learners?filter=unsubmitted は200を返している。 responseには、submittedAt が入っている受講者も含まれている。

この情報があると、少なくとも通信自体は失敗していないと分かる。 statusが200で、responseの中身が期待と違うなら、画面表示だけの問題ではなく、API側かデータ整形の処理に近い問題かもしれない。

再現ログは、修正後にも使う。 同じ手順をもう一度実行し、期待結果へ変わったかを確認するためである。

観察と推測は、別々に書く。

種類 書き方の例
観察した事実 GET /api/mentor/learners?filter=unsubmitted は200を返した
観察した事実 responseに submittedAt が日時の受講者が含まれていた
推測 API側のfilter条件が逆かもしれない
まだ不明 画面側で追加のfilter処理をしているかは未確認

事実と推測を混ぜると、相談された人はどこまで確認済みなのか判断できない。 再現ログでは、見たもの、考えたこと、まだ見ていないものを分ける。

観察結果から、読む範囲を小さくする

既存コードを読むときは、最初に知りたいことを決める。 この不具合で知りたいのは、未提出者フィルタを選んだとき、どのAPIが呼ばれ、どこで提出済みの受講者が混ざるのかである。

第7章のHTTP観察を使うと、読む場所を絞れる。 まず、NetworkタブでAPI requestを見る。 filter=unsubmitted がqueryに入っているかを見る。 statusが200か、400番台か、500番台かを見る。 response bodyに、期待と違うデータが含まれているかを見る。

今回の例で、GET /api/mentor/learners?filter=unsubmitted が200を返し、responseに提出済み受講者が含まれているなら、次に見るべき候補はAPI側のfilter処理である。 画面のボタンが壊れている可能性はゼロではないが、queryがAPIに届いているなら、優先順位は下がる。

読む順番は、たとえば次のように絞れる。

観察 優先して読む場所 理由
request自体が出ていない 画面側のイベント処理 操作がAPI呼び出しまで届いていない可能性がある
request URLに filter=unsubmitted がない 画面側のquery生成 フィルタ条件を付けていない可能性がある
request URLにqueryがあり、statusは200 API handlerとfilter処理 APIは呼ばれているが、返す内容が期待と違う可能性がある
statusが500 API handler、ロジック、サーバーログ サーバー内部で例外が起きている可能性がある
responseは正しいが画面表示が違う 画面側の描画処理 API後の表示変換でずれている可能性がある

観察結果は、読む順番を決めるために使う。 読む範囲を狭めることは、手抜きではない。 実務のコードでは、すべてを理解してから直す進め方は現実的ではない。 今回の不具合に関係する流れを先に追う。

検索語は、画面と通信から作る

コード探索の入口は、見えている言葉から作る。 検索語とは、関連コードを探すために使うURL、API名、画面文言、field名、エラーメッセージなどの手がかりである。

今回なら、検索語は次のように作れる。

  • /mentors/progress
  • /api/mentor/learners
  • submittedAt
  • unsubmitted
  • filter

ターミナルなら、rg を使って検索できる。 rgripgrepの略で、プロジェクト内の文字列を高速に探す道具である。 たとえば、次のように検索する。

rg "/mentors/progress|/api/mentor/learners|submittedAt|unsubmitted|filter" starter-apps/learning-log-sample

検索結果は、種類に分けて読む。 画面に関係するファイル、API handler、条件分岐のロジック、データ、テストを分ける。 研修用サンプルアプリなら、src/server.jssrc/public/app.jssrc/store.jstest/server.test.jstest/store.test.js のような候補が出る。

このサンプルアプリでは、候補ファイルを次のように見られる。

種類 ファイル この不具合で見ること
画面 src/public/app.js フィルタ選択から filter=unsubmitted をqueryへ入れているか
API handler src/server.js /api/mentor/learners がqueryを読み、どの関数へ渡しているか
ロジック、データ src/store.js listLearnerssubmittedAt をどう判定しているか
APIテスト test/server.test.js API全体の確認がどこまであるか
ロジックテスト test/store.test.js filter条件のテストがあるか、足りない観点は何か

最初からすべてのファイルを読む必要はない。 まず、URLがどのhandlerへつながるかを見る。 次に、handlerがどの処理を呼ぶかを見る。 その処理が、データの submittedAt をどう扱うかを見る。 必要なら、同じ条件を扱うテストを見る。

呼び出しの流れを、少しずつ追う

入口が見つかったら、呼び出し元と呼び出し先を少しずつ追う。 画面からAPI、APIからロジック、ロジックからデータ、必要ならテストへ進む。

今回の題材なら、流れは次のように追える。

画面で未提出者フィルタを選ぶ
-> src/public/app.js の loadLearners が URLSearchParams に filter を入れる
-> fetch が /api/mentor/learners?filter=unsubmitted を呼ぶ
-> src/server.js が query を読み、listLearners に渡す
-> src/store.js の listLearners が submittedAt で絞り込む
-> JSON response が画面へ返る

このとき大切なのは、今回の不具合に関係する流れをつなぐことである。 関係が薄い設定ファイル、別画面、別APIまで広げると、調査が散らばる。 気になる場所はメモして後回しにする。

code-reading-note.mdには、探した入口、見つけた候補ファイル、呼び出しの流れ、まだ読めていない場所を書く。 完全な理解を装う必要はない。 むしろ、まだ読めていない場所を残すことで、レビューや相談で確認できる。

ログやエラーメッセージがある場合は、そこからも入口を探す。 スタックトレースは、エラーがどの呼び出しの流れで起きたかを示す一覧である。 全部を追う必要はない。 まず自分たちのアプリのファイル名を探し、最初に失敗した行と、その行を呼んだ場所を見る。 借りているライブラリの中を追い続けるより、自分が修正できる場所を探す。

ログやスタックトレースを読むときも、事実と推測を分ける。

見るもの 読み方
エラーメッセージ 何が失敗したと書かれているか
最初に出た自分たちのファイル 自分が調査できる入口はどこか
request URLやmethod どの操作と対応しているか
入力値やID 再現ログの操作と一致しているか
secretらしい値 記録やAI入力に貼らず伏せる

仮説は、消せる形で書く

仮説は、観察した事実から考えた原因候補である。 原因の断定ではない。 良い仮説には、どう確認すれば正しいか、または違うと分かるかがある。

今回の不具合なら、仮説は次のように書ける。

仮説1: API側が filter=unsubmitted を読んでいない。 確認方法は、API handlerでquery parameterを読んでいる箇所を探すこと。

仮説2: API側はfilterを読んでいるが、submittedAt の条件が逆になっている。 確認方法は、learner.submittedAt === nulllearner.submittedAt !== null のような条件分岐を見ること。

仮説3: 画面側でAPIの結果を上書きしている。 確認方法は、API responseと画面表示を比べ、画面側に追加filter処理があるかを見ること。

仮説4: テストデータの submittedAt が想定と違う。 確認方法は、サンプルデータとテストデータの値を見ること。

hypothesis-log.mdでは、観察した事実、原因候補、確認方法、確認結果、判断を表にする。 仮説が外れたことも成果である。 その確認によって、次に見なくてよい場所が分かるからである。

確認結果は、次のように書ける。

原因候補 確認方法 結果 判断
画面がqueryを付けていない src/public/app.js とNetworkタブを見る request URLに filter=unsubmitted が付いていた 消す
API handlerがfilterを渡していない src/server.js/api/mentor/learners を見る url.searchParams.get("filter")listLearners に渡していた 消す
filter条件が逆 src/store.jslistLearners を見る filter === "unsubmitted" の条件で submittedAt !== null を残していた 残す

この表は、答えをきれいに見せるためではない。 どの可能性を確認し、なぜ残したのかを、レビューで追えるようにするためである。

AIを使う場合も、原因候補の洗い出しや検索語の提案に使う。 ただし、AIの候補を原因として採用する前に、実際のコード、実行結果、テストで確認する。

最小修正は、原因に対応する範囲へ絞る

原因が見えてきたら、修正範囲を小さくする。 最小修正とは、原因に対応する範囲へ絞った変更である。 小さい修正は、臆病な修正ではない。 レビューしやすく、戻しやすく、回帰確認しやすい修正である。

今回の原因が、API側の未提出者filter条件の誤りだと分かったとする。 その場合、修正は filter=unsubmitted のときに submittedAtnull の受講者だけを返す処理へ絞る。 同じPRで画面文言を大きく変えたり、データ構造を整理したり、別のフィルタ仕様まで変えたりしない。

修正方針は、コードを書く前に一文で置く。

修正方針:
`filter=unsubmitted` の場合だけ、`submittedAt === null` の受講者を残す。
既存の `submitted` filter、supportStatus filter、支援ステータス更新APIは変更しない。

この一文があると、差分が方針から広がっていないか確認しやすい。 もし別の関数名整理や表示文言変更が必要に見えても、このPRでやるかどうかを判断できる。

不具合修正とリファクタリングを同時に広げると、レビューで何を確認すべきか分からなくなる。 構造整理が必要なら、バグ修正のPRとは分ける。 第6章で扱ったように、PRは変更提案である。 提案の目的が一つなら、レビューも判断もしやすい。

修正したら、git diff で差分を見る。 差分は、変更前と変更後の違いであり、レビューの中心材料である。 自分で差分を説明できない変更は、まだPRに出せない。

再現手順で確認し、回帰確認を足す

修正後は、最初に書いた再現手順をもう一度実行する。 未提出者フィルタで、未提出者だけが表示されるかを見る。 API response自体が、submittedAt: null の受講者だけになっているかを見る。 画面だけで提出済み受講者を隠していないか確認する。

回帰確認は、直した不具合が戻っていないことと、近くの正常系が壊れていないことを確かめる確認である。 今回なら、少なくとも次を見る。

  • 未提出者フィルタで未提出者だけが表示される。
  • フィルタなしでは全件が表示される。
  • 提出済みフィルタがある場合、提出済みだけが表示される。
  • supportStatus filterと組み合わせた場合に、条件が意図どおり重なる。
  • API responseが期待どおりである。
  • 既存テストがあれば通る。

この章では、テスト設計を深く扱わない。 ただし、既存テストがあるなら実行する。 研修用サンプルアプリなら、npm test でNode.js組み込みテストを実行できる。

確認コマンドは、次のように記録できる。

npm test
curl -i "http://localhost:3000/api/mentor/learners?filter=unsubmitted"
curl -i "http://localhost:3000/api/mentor/learners"

既存テストが通っても、今回の不具合を覆うテストがなければ安心しすぎない。 可能なら、listLearners({ filter: "unsubmitted" })submittedAt === null の受講者だけを返すテストを追加する。 テスト追加がこの章の主目的でなくても、「既存テストにこの観点はなかった」と記録するだけで、レビュー時の判断材料になる。

fix-verification-log.mdには、作業branch、修正方針、変更ファイル、差分確認、再現手順での確認、実行したテスト、まだ不安なことを書く。 まだ不安なことを書いてよい。 何を確認し、何を確認していないかが分かれば、レビューで判断できる。

修正PRは、調査と判断を共有する

バグ修正のPull Requestでは、コード差分だけでなく、調査の流れも伝える。 レビュアーが見たいのは、変更した行だけではない。 この修正でよいと判断した理由である。

bug-fix-pr-note.mdには、次を書く。

  • PR URL。
  • 再現手順。
  • 期待結果。
  • 実際の結果。
  • 原因。
  • 修正内容。
  • 確認方法。
  • 影響範囲とリスク。
  • レビューしてほしい点。
  • AIを使った場合の検証。

たとえば、原因は次のように短く書ける。

原因: GET /api/mentor/learners?filter=unsubmitted は呼ばれていましたが、API側の絞り込みで submittedAtnull の受講者を返す条件になっていませんでした。

修正内容: filter=unsubmitted の場合は、submittedAtnull の受講者だけを返すようにしました。

確認方法: 未提出者フィルタで未提出者だけが表示されることを確認しました。 フィルタなしで全件が表示されることを確認しました。 既存テストを実行しました。

影響範囲、リスク: listLearners のfilter条件のみを変更しました。 進捗一覧APIの filter=unsubmittedfilter=submitted に影響します。 supportStatus filterや支援ステータス更新APIは変更していません。

レビューしてほしい点: 未提出者と提出済みの判定が submittedAt の有無でよいかを確認してほしいです。 既存テストに未提出者filterの観点を追加する必要があるかも見てほしいです。

このように書くと、レビュアーは再現、原因、修正、確認を一続きで追える。 第7章のHTTP観察ログ、第6章のPR本文、第5章のローカル実行ログがここでつながる。

AIは、探索と確認の補助として使う

AIは、既存コードのデバッグでも役に立つ。 検索すべき文字列、原因候補、スタックトレースの読み方、テスト観点、PR本文の抜け漏れ確認を出してもらえる。

ただし、再現前に修正コードを書かせると、観察より先に答えらしきものへ飛びつきやすい。 AIに最初に頼むなら、修正案ではなく、原因候補と確認方法を出してもらう。

依頼は次のように書ける。

再現: 未提出者フィルタを選んでも、提出済みの受講者が一覧に残ります。

観察: GET /api/mentor/learners?filter=unsubmitted は200です。 responseには submittedAt が入っている受講者も含まれています。 Cookieや個人情報は含めていません。

出してほしいこと: 原因候補を3つ、それぞれの確認方法、まず検索すべき文字列。

注意: 修正コードはまだ出さないでください。 不確かなことは断定しないでください。

AIの回答は、仮説として扱う。 採用する前に、差分、再現手順、テスト、実行結果で確認する。 秘密情報、Cookie、token、個人情報は渡さない。

AIに修正案を出してもらう段階へ進む場合も、渡す情報を絞る。 再現ログ、HTTP観察、候補ファイル、確認済みの仮説を渡し、未確認の推測を確定事実として書かない。 AIが大きなリファクタリングや無関係なUI変更を提案したら、今回の不具合に必要な範囲へ戻す。

再現と仮説で確認すること

reproduction-log.md、code-reading-note.md、hypothesis-log.md、fix-verification-log.md、bug-fix-pr-note.md を作る。

reproduction-log.md には、再現手順、期待結果、実際の結果、Network、response、サーバーログを書く。 code-reading-note.md には、探した入口、候補ファイル、呼び出しの流れ、まだ読めていない場所を書く。 hypothesis-log.md には、観察した事実、原因候補、確認方法、確認結果、残った仮説を書く。 fix-verification-log.md には、修正方針、変更ファイル、差分確認、再現確認、回帰確認、実行したテストを書く。 bug-fix-pr-note.md には、PR URL、再現、期待結果、実際の結果、原因、修正内容、確認方法、影響範囲とリスク、レビューしてほしい点、AIを使った場合の検証を書く。

成果物の目的は、調査した量を見せることではない。 どの事実から、どの仮説を残し、どの修正を選び、どう確認したかを追えるようにすることである。

既存コード調査で起きやすい誤解

  • 再現できないままコードを直し始める。
  • 画面の印象だけで、APIやログを確認しない。
  • コード全体を読もうとして、今回の不具合に関係する入口を見失う。
  • 仮説と事実を同じ文で混ぜる。
  • 仮説が外れたことを失敗だと思い、記録しない。
  • 既存テストが通っただけで、今回の不具合が覆われたと思い込む。
  • API responseが間違っているのに、画面側だけで隠して直したことにする。
  • 不具合修正とリファクタリングを同じPRに混ぜすぎる。
  • 修正後に、元の再現手順だけ確認して、周辺の正常系を見ない。
  • AIが出した修正を、差分確認なしにcommitする。

既存コードを読む章で持ち帰ること

第8章で身につけるべきことは、既存コードを手がかりから小さく読むことである。 不具合は、まず再現する。 画面、Network、curl、サーバーログで観察する。 URL、API path、field名、画面文言、エラーメッセージから検索語を作る。 仮説は、確認できる形で複数出し、外れたものを消していく。 修正は原因に対応する最小範囲にし、再現確認と回帰確認を添える。

良いデバッグは、勘の良さだけで決まらない。 観察した事実を残し、次に読む場所を絞り、判断をPRで説明できることが重要である。

要件からドメインを整理する章へ

次章からは、既存の題材をもとに、新しい支援ステータス機能を設計する。 第8章で扱った、期待結果と実際の結果を分ける力、用語を検索語にする力、事実と仮説を分ける力は、第9章の要件とドメイン整理でも使う。

参考資料

教材を検索