Givery教材トップシステムの歩き方事前セットアップ
DAY 2 2026年9月9日(水)

頼み方を型にして、テストで裏を取る

前回は全部ふつうの日本語で頼んで動かしました。今日はその頼み方を型にして、出てきたものを自分で確かめられるところまで持っていきます。題材は Java ですが、読み書きできる必要はありません。見るのは「何が変わったか」と「動かして何が出たか」の2つです。

この日の演習

演習は上から順に進みます。全員が終わるのを待ってから次へ進むので、詰まったら手を挙げてください。早く終わった方には各演習に発展課題があります。

D2-0指示の型[15min]
ねらい

前回は全部ふつうの日本語で頼みました。うまくいった依頼と、やり直しになった依頼の違いを型にして、今日から自分で再現できるようにします。

この演習で扱うもの

前回、みなさんは Claude Code にふつうの日本語で頼んで、動くものを作りました。専門用語はほとんど使っていません。それで通りました。

ただ、一発で通った依頼と、何度もやり直した依頼があったはずです。その違いを言葉にしておかないと、次に同じことが再現できません。今日はそこを整理します。

なぜこれをやるのか

AI は察してくれません。書いていないことは、良かれと思って埋めにきます。前回の D1-4 で、頼んでいない親への参照が入ってきたのがそれです。

逆に言えば、書いておけば防げます。何を書いておけばいいのかが分かれば、毎回同じ品質で頼めるようになります。それが今日の15分の目的です。

この型に Java の知識は入っていません。COBOL でも Python でも C# でも同じ形で使えます。

ここまで出たら完了
  • 自分の言葉で「対象・やること・やらないこと・終わりの合図」の4つを言える
  • 前回の依頼を1つ選んで、4つが入った形に書き直したものが、紙かメモ帳にある
  • 長く書くほど良くなるわけではない、と説明できる
さわるファイル
読むdocs/生成物チェックリスト.md採否の基準はここに集めてある
手を動かす
1
考える
前回いちばん手間取った依頼を1つ思い出してください。長く書いたのに違うものが出てきたか、短く書いたのに一発で通ったか。どちらだったかを覚えておきます。
2
見る
うまくいった依頼には4つが入っています。対象(どのファイルの話か)、やること(何ができればいいか)、やらないこと(何を混ぜてほしくないか)、終わりの合図(どうなったら完了か)。この4つです。揃っていない依頼がやり直しになります。長さは関係ありません。
3
書く
前回の D1-4 を、この4つの形に書き直してみてください。紙でもメモ帳でも構いません。答えの一例はこうです。「@OrderItem.java @OrderDTO.java 受注の中身も一緒に返るようにしてください。元の受注に戻る参照は入れないでください。/api/orders/1 に items が出れば完了です」。前回との違いは、3つ目と4つ目を足しただけです。
4
実行
書き直した依頼を Claude Code に送るところまではしません。この形を、このあとの D1-5 と D2-1 でそのまま使います。今日は毎回この4つを口に出してから送ってください。
考えること
4つのうち「やってほしくないこと」を書かなかったら、何が起きるか。前回の D1-4 で親への参照が混ざった件が実例になります。
AI の出方
同じ依頼でも返ってくるコードは毎回変わります。変わらないのは、4つが揃っている依頼ほど直しが少ないという点です。前回そう感じなかった方は、そのまま言ってください。合わない型は使わなくて構いません。
D2-0+docs/生成物チェックリスト.md を開き、自分の現場で使えそうな項目に印を付けてください。ここに書いてあるのは Java 向けの言い回しですが、確認する観点そのものは言語を選びません。COBOL でも Python でも C# でも、同じ観点で読めます。自分の言語ならどう言い換えるかを、1項目だけ書き換えてみてください。
くわしく(背景・詰まったときの対処)

前回はあえて型を出さずに進めました。まず通じることを体験してもらうためです。実際、ふつうの日本語で頼んで動きました。

ただ、それだけだと「たまたま通った」で終わります。持ち帰って明日も使えるようにするには、何が効いていたのかを言葉にしておく必要があります。

効いていたのは4つです。1つ目、どのファイルの話かを @ で指定したこと。付けないと探し回るので時間がかかり、関係ないファイルまで直されることがあります。2つ目、やってほしいことを1つに絞ったこと。2つ以上を1回で頼むと、片方だけ直った状態で返ってきます。3つ目、やってほしくないことを添えたこと。前回の D1-4 で「元の受注に戻る参照は入れないで」と言わなかった場合、見た目は動くのに壊れたものが出ます。4つ目、どうなったら終わりかを決めておいたこと。これが無いと、実装が済んでいるのに終わったかどうか分からなくなります。前回まさにここで止まりました。

この4つは、Java の知識とは関係ありません。COBOL でも Python でも C# でも同じです。今日はこの形を使って進めます。

D1-5ステータス遷移ルールの実装[25min]
ねらい

前回できなかった分です。確定済みの受注もキャンセルできるように直して、自分の画面のボタンから実際に通します。

ここまで出たら完了
  • 画面のボタンから id=2 を CANCELLED にでき、再読み込みすると一覧の id=2 が CANCELLED で表示される
  • CONFIRMED の id=3 を SHIPPED に変える操作は、今までどおり通る
  • SHIPPED の id=4 を CANCELLED にしようとすると通らない(共通エラーハンドラが無いので 500 で返る)
画面のボタンから id=2 を CANCELLED にして、再読み込みした状態。一覧の id=2 が CANCELLED になっていれば達成です
画面のボタンから id=2 を CANCELLED にして、再読み込みした状態。一覧の id=2 が CANCELLED になっていれば達成です
さわるファイル
書くsrc/main/java/com/example/order/service/OrderServiceImpl.javaCONFIRMED の遷移を直す
書くsrc/main/resources/static/dashboard.htmlステータス変更ボタンを足す
手を動かす
1
考える
受注の状態は、受付前・確定・発送済み・納品済み・キャンセルの5つで、進む順番が決まっています。いまは受付前からしかキャンセルできません。確定からもできるように足しますが、確定から発送済みへ進む道は残す必要があります。ここを先に決めてから頼みます。
2
書く
2枚目のターミナルで claude を起動し、次のように送ります。「@src/main/java/com/example/order/service/OrderServiceImpl.java の validateStatusTransition で、確定済み(CONFIRMED)からキャンセル(CANCELLED)にもできるようにしてください。発送済み(SHIPPED)に進める動きは今までどおり残してください。」最後の一文が「やらないこと」にあたります。これが無いと、進める道ごと書き換えた案が返ります。
3
書く
続けて画面側です。「@src/main/resources/static/dashboard.html に、表の行から PATCH /api/orders/{id}/status を叩いてステータスを変更できるボタンを足してください。/cancel ではなく /status を使ってください。」と送ります。最後の一文は必ず入れてください。/cancel は遷移ルールを通らないので、直す前でも成功してしまい、演習になりません。
4
答え合わせ
Java を直したので、1枚目のターミナルで Ctrl+C を押してアプリを止め、mvn spring-boot:run で起動し直します。起動したらブラウザで http://localhost:8080/dashboard.html を開き直し、画面のボタンから id=2 を CANCELLED にします。続けて id=3 と id=4 でも試します。
考えること
InvalidOrderStateException は業務エラーなのに 500 で返る。本来はどの HTTP ステータスが正しいか。
AI の出方
文字列の "CANCELLED" を直に埋め込む案が出ます。Order.STATUS_CANCELLED の定数に直っているかを変更内容で確かめてください。case ブロックごと書き換えて SHIPPED への遷移を消してしまう案も出るので、そこも見ます。画面側では PATCH /api/orders/{id}/cancel を叩くボタンが出ることがあります。cancelOrder は遷移ルールを通らず、直す前でも 200 を返してしまうので、fetch のパスが /status になっているかを確かめてください。
D1-5+@RestControllerAdvice の共通エラーハンドラを足し、OrderNotFoundException を 404、InvalidOrderStateException を 400 に対応づけてください。実装後に1枚目のターミナルで Ctrl+C を押し、mvn spring-boot:run で起動し直してから SHIPPED の受注を叩くと、500 だったものが 400 に変わります。余力があれば updateOrder と deleteOrder に、PENDING 以外を弾くステータスチェックも足してみてください。
くわしく(背景・詰まったときの対処)

OrderServiceImpl の validateStatusTransition は、受注ステータスをある値から別の値へ変えてよいかを判定するメソッドです。配布時点では case Order.STATUS_CONFIRMED が SHIPPED への遷移しか許していないため、確定済みの受注をキャンセルできません。業務ルールでは PENDING と CONFIRMED からのキャンセルを認めます。出荷済み以降は認めません。モノが動いたのに帳簿だけ消える状態になるためです。

確認は PATCH /api/orders/{id}/status で行います。内部で validateStatusTransition が呼ばれるからです。PATCH /api/orders/{id}/cancel は cancelOrder という別のメソッドを呼ぶので、遷移ルールの確認には使いません。手順3で作るボタンも、押したときに叩くのは /status のほうです。

ステータス値は文字列の直書きではなく Order の定数を使います。綴り間違いをコンパイルの時点で見つけられるからです。

画面のボタンがうまく動かないときは、ターミナルから直接叩いても確かめられます。

打つのは次の1行です。curl -X PATCH http://localhost:8080/api/orders/2/status -H "Content-Type: application/json" -d '{"status":"CANCELLED"}'

返ってきた status が CANCELLED になっていれば通っています。

配布データでは id=2 と id=3 が CONFIRMED、id=4 が SHIPPED で入っています。id=2 を一度 CANCELLED にすると遷移元が変わるので、やり直すときはアプリを起動し直してください。H2 はメモリ上で動くので、起動のたびに配布時の10件へ戻ります。

新しいファイルは作りません。編集は OrderServiceImpl.java の中と、自分のダッシュボードだけです。手順4で起動し直すのは OrderServiceImpl を直したためで、ボタンを足した HTML のほうは再読み込みだけで反映されます。ボタンの見た目や文言をあとから直すときは、起動したまま再読み込みで確かめてください。

D2-1テスト観点を出してから書かせる[35min]
ねらい

何を確かめるべきかを先に自分で書き出し、それを AI と詰めてから、テストコードを書かせます。

この演習で扱うもの

扱うのは changeStatus という機能です。受注の状態を別の状態へ変える処理で、受注管理システムの中で唯一、業務ルールで動きが決まっている場所です。

状態は5つあります。受付前(PENDING)、確定(CONFIRMED)、発送済(SHIPPED)、納品済(DELIVERED)、キャンセル(CANCELLED)。進める順番が決まっていて、受付前から確定へ、確定から発送済へ、発送済から納品済へ。キャンセルは受付前と確定からだけできます。発送したあとは取り消せません。

この機能に渡ってくるのは、受注の ID と、変更したい状態の2つだけです。返るのは変更後の受注か、ルールに反していれば例外です。入口と出口がはっきりしているので、何を確かめるべきかを考えやすい題材です。

なぜこれをやるのか

AI にテストを書かせると、量はいくらでも出ます。ただ、何を確かめるかは決めてくれません。「テストを書いて」とだけ頼むと、いま動いている通りに動くことを確かめるテストが返ります。それは仕様と合っているかを見ていないので、バグがあればバグごと固定してしまいます。

だから順番が逆なんです。先に人が観点を決めて、それを渡す。観点さえ決まっていれば、コードを書くのは AI のほうが速くて正確です。人が持つべきなのは「何を確かめるか」で、「どう書くか」ではありません。

ここまで出たら完了
  • 自分で書き出した観点リストが手元にある(AI に見せる前のものと、確定後のものの両方)
  • AI の指摘のうち、採用したものと採用しなかったものを言える
  • Tests run が配布時の9件から、自分が出した観点のぶん増えている
  • 赤が残っていれば、それがテストの誤りか実装の穴かを説明できる
手順1。配布時のまま走らせた状態。赤1件が正常です
手順1。配布時のまま走らせた状態。赤1件が正常です
完了時。Tests run が 9 から増えていれば達成です
完了時。Tests run が 9 から増えていれば達成です
さわるファイル
書くsrc/test/java/com/example/order/service/OrderServiceImplTest.javaTODO 4件をテストに置き換える
書くsrc/main/java/com/example/order/service/OrderServiceImpl.java遷移ルールの確認と一時的な書き換え
読むdocs/生成物チェックリスト.md採否の基準。既知バグの赤は除く
まず自分で考える [5min]

紙かメモ帳に、changeStatus で確かめるべきことを書き出してください。コードは見なくて構いません。上の状態遷移の説明だけで書けます。目標は10個です。

3つの向きから探すと出ます。

  • 正常系 … 通るはずの遷移。受付前から確定へ、確定から発送済へ、など
  • 異常系 … 通ってはいけない遷移。発送済からキャンセル、納品済から受付前へ戻す、など
  • 境界・特殊 … 同じ状態へ変える、存在しない ID、存在しない状態の名前、キャンセル済みからの変更
AI と詰める

書き出した観点を Claude Code に見せて、抜けを指摘させます。ここで初めて AI を使います。自分のリストをそのまま貼ってください。

返ってきたもののうち、納得したものだけ採用してください。全部採ると自分の判断が消えます。1〜2往復で観点を確定させます。

@src/main/java/com/example/order/service/OrderServiceImpl.java
受注の状態を変える changeStatus について、確かめるべき観点を自分で書き出しました。

(ここに自分のリストを貼る)

抜けている観点があれば指摘してください。まだテストコードは書かないでください。
指摘は理由も一緒に、箇条書きでお願いします。
手を動かす
1
実行
先に、いまのテストがどうなっているかを見ます。2枚目のターミナルで cd ~/handson してから mvn test -Dtest=OrderServiceImplTest と打ちます。Tests run: 9, Failures: 1 で終われば正常です。赤1件は配布時からの仕込みで、壊れていません。
2
書く
確定した観点を渡して、テストコードを書かせます。「(確定した観点リスト)この観点でテストを実装してください。エラーになるはずのケースは、エラーの種類まで確かめてください。保存が呼ばれたかどうかも確かめてください。」と送ります。観点をそのまま渡すのがポイントで、こちらの意図が全部入ります。
3
答え合わせ
出てきたテストと、自分の観点リストを1対1で突き合わせてください。書いた観点が全部テストになっているか。なっていない観点があれば「◯◯の観点が入っていません。足してください」と1つずつ足します。
4
実行
mvn test -Dtest=OrderServiceImplTest をもう一度打ちます。Tests run が9件から自分の足したぶん増えます。赤が出たら、テストが間違っているのか実装が間違っているのかを切り分けてください。今日の題材では、CONFIRMED からキャンセルできるかどうかで結果が変わります。
考えること
自分が出せなかった観点は、なぜ出せなかったか。仕様を知らなかったのか、考える向きが足りなかったのか。
AI の出方
観点を先に渡すと、返ってくるテストの形が安定します。渡さない場合との差を見てください。指摘には「同値分割」「境界値分析」のような用語が出てくることがありますが、意味は「似た入力はまとめて1つ試す」「変わり目の値を試す」です。用語は覚えなくて構いません。黙って @Disabled が付いて赤が隠れることがあるので、付いていたら外してください。
D2-1+同じ手順を createOrder でやってみてください。受注を新しく作る処理です。changeStatus より入力が多いぶん、観点も増えます。数量が0のとき、単価が負のとき、納品予定日が過去のとき。この3つは配布物では TODO のまま残してあります。 ここで分かるのは、観点を出しても実装がなければテストは通らない、という順番です。検証は OrderCreateRequest の注釈と Controller の @Valid でしか動かないので、サービスを直接呼ぶ単体テストでは走りません。落ちる前提で書いてから、サービス層に検証を足すか Controller 経由のテストに変えるかを AI に切り分けさせてください。
くわしく(背景・詰まったときの対処)

OrderServiceImplTest は findById・createOrder・changeStatus・cancelOrder・findAll の代表ケースが正常系・異常系とも実装済みで、changeStatus の @Nested に4件の TODO コメントが残っています。CONFIRMED から SHIPPED、SHIPPED から DELIVERED、CANCELLED からの遷移は全部不可、同一ステータスへの遷移は不可、の4件です。テスト基盤は組んであるので @ExtendWith や @Mock を書き足す必要はありません。AssertJ の assertThat と assertThatThrownBy、Mockito の when と verify、既存テストと同じ書き方に揃えるだけです。追記先は既存ファイルで、新規ファイルは作りません。

mvn test -Dtest=OrderServiceImplTest は9件中1件が赤です。Day2 で初めてテストを走らせると赤が1件出ますが、これは配布時点で仕込んである失敗で、環境の不備ではありません。DELIVERED の受注をキャンセルできてしまう穴が cancelOrder に残っていて、例外が出ずにキャンセルが通り、スタブしていない save が null を返すので、続く行で NullPointerException になります。画面に出るのは expecting ... InvalidOrderStateException but was ... NullPointerException の行です。Day3 の題材なので、今日は直しません。走らせる前に配布時点の状態を一度見ておくと、自分が足したテストの失敗と切り分けやすくなります。

@ を打つと候補が出るので、ファイル名でもパスでも選べます。実行結果の読み方には癖がひとつ。Maven のコンソールに出るのはクラス単位の集計行と落ちたテストの名前だけで、通ったテストのメソッド名は表示されません。名前まで確かめたいときは target/surefire-reports/TEST-com.example.order.service.OrderServiceImplTest$ChangeStatusTest.xml を開き、testcase name に自分が足したメソッド名が並ぶのを見てください。

同一ステータスへの遷移には専用のチェック文がありませんが、どの case も自分自身を遷移先に挙げていないので例外が出ます。このテストは配布時点でも緑です。もうひとつ、CONFIRMED から CANCELLED が成功する前提のテストは、Day1 の D1-5 を済ませていなければ落ちます。落ちたときは validateStatusTransition の CONFIRMED の case を開いて、SHIPPED しか許していないことを自分で確かめてください。生成物の誤りなのか実装の穴なのかは、ここが分かれ目です。採否の基準は docs/生成物チェックリスト.md の B-5 に書いてあります。

休憩

[10min] ここで一度手を止めます。詰まっている方はこの間に声をかけてください。

D2-2バグを先にテストで捕まえる[30min]
ねらい

仕様と実装のどちらが正しいかを自分で判断してから、テストで赤を出して直します。

この演習で扱うもの

扱うのは BuggyOrderCalculator という受注計算のクラスです。数量に応じた割引、顧客ごとの合計、納品の遅延チェックの3つを持っています。

仕様はコメントに書いてあります。10個以上で5%割引、20個以上で10%、50個以上で15%。該当する受注がない顧客は合計0円。納品予定日の当日も遅延に含める。

この3つに、それぞれ実装のずれが仕込んであります。動きはしますが、仕様どおりではありません。

なぜこれをやるのか

AI にテストを書かせるとき、いちばん危ないのが「いま動いている値」を期待値にすることです。バグがあるコードにテストを書かせると、バグごと正解として固定されます。以後、誰かが直そうとすると赤くなるので、直せなくなります。

これを防ぐ方法は1つで、期待値を仕様から取ることです。そのためには仕様を読む必要があり、読むのは人の仕事です。

順番も大事です。先に赤を出してから直す。逆にすると、何が直ったのか、そもそも壊れていたのかが分からなくなります。

ここまで出たら完了
  • 自分で計算した3つの期待値と、AI が書いた期待値の違いを言える
  • 修正前に赤が出て、expected: 475000 と but was: 500000 を自分の画面で読んだ
  • 3か所を直したあと、Failures も Errors も 0 になる
  • 演算子を元に戻すと赤が戻ることを確かめ、また直して緑に戻した
手順3。直す前にこの赤を出すのが目的です
手順3。直す前にこの赤を出すのが目的です
完了時。Failures も Errors も 0 になります
完了時。Failures も Errors も 0 になります
さわるファイル
書くsrc/main/java/com/example/order/buggy/BuggyOrderCalculator.javaexercises から移動。修正もここ
新規src/test/java/com/example/order/buggy/BuggyOrderCalculatorTest.java生成させるテストの置き場所
まず自分で考える [5min]

コードのコメントに書かれた仕様を読んで、期待値を自分で計算してください。この3つが、あとで判断の基準になります。

  • 数量10個・単価5万円のとき、割引後はいくらか(仕様どおりに計算する)
  • 受注が1件もない顧客の合計は、いくらか。それとも何も返さないのが正しいか
  • 納品予定日がちょうど今日の受注は、遅延に入るか入らないか
AI と詰める

テストを生成させます。ここで出てきた期待値と、自分が計算した値を1つずつ突き合わせてください。

違っていたら、AI が「いまのコードが返す値」を書いています。自分の計算した値に手で直してください。ここが今日いちばん大事な操作です。

/gen-test @BuggyOrderCalculator.java

(出てきたら、置き場所とケースを1つずつ足していく)
数量10ちょうどのケースも見てください。
テストは src/test/java/com/example/order/buggy/ に置いてください。
手を動かす
1
実行
ターミナル(claude を起動していたら Ctrl+D か /exit で一度抜けます)で mkdir -p src/main/java/com/example/order/buggy を打ち、続けて mv exercises/day2/BuggyOrderCalculator.java src/main/java/com/example/order/buggy/ を打ちます。Maven は src/main/java の下しか読まないので、この一手がないとテストがコンパイルで止まります。
2
書く
claude を起動して /gen-test @BuggyOrderCalculator.java だけを送り、出てきたテストの置き場所が src/test/java/com/example/order/buggy/ でなければ「buggy パッケージに置いて」と1行足します。数量10ちょうどのケースが無ければ「数量10ちょうども見て」と足す、というふうに1回に1つずつ条件を積みます。
3
答え合わせ
ここが今日いちばん大事なところです。出てきたテストの期待値が、仕様どおりの値になっているか、それとも「いまのコードが返す値」をそのまま書いただけかを1件ずつ見ます。後者だと、バグごとテストで固定してしまい、以後だれも気づけません。calculateDiscount(10, 50000) が 500000 になっていたら、仕様の 475000 に手で直してください。claude を抜けて mvn test -Dtest=BuggyOrderCalculatorTest を打つと、expected: 475000 と but was: 500000 が出ます(BUILD FAILURE で終わりますが、想定どおりです)。
4
書く
比較演算子、total の初期値、日付比較の3か所をまとめて直し、もう一度走らせて全部緑になるのを見ます。緑になったら比較演算子だけ > に戻して赤が戻るのを確かめ、>= に直し直して緑に戻してから終わります。
考えること
10個ちょうどで割引が乗らないのは、仕様の読み違いか実装の書き間違いか。そう判断した根拠はどこにあるか。
AI の出方
期待値を仕様ではなく現在の実装値で埋めてくることがあります。生成したテストが最初から全部緑なら、それはバグごと固定された合図なので「期待値は仕様の値で書いて。いまの実装が返す値に合わせないで」と言い直してください。getCustomerOrderTotal は戻り値が null になるので、int で受ける書き方だと Failures ではなく Errors 側に出ます。
D2-2+直した3か所に再発防止のテストを足します。境界ちょうどの 10・20・50、複数受注を持つ東京電機工業の合計、SHIPPED の ORD-001 が遅延に入らないこと。演算子を > に戻したときに赤で気づけるのは境界ちょうどの3点で、手前と直後(9・11、19・21、49・51)は > でも >= でも同じ値を返すため、こちらは修正で別の境界を壊していないかの確認に使います。あわせて、3か所をまとめてではなく1か所ずつ直して都度テストを走らせる進め方も試してください。
くわしく(背景・詰まったときの対処)

BuggyOrderCalculator は受注5件を内部に持つ単体クラスで、公開メソッドは3つです。仕様は Javadoc に書いてあります。数量10個以上で5%、20個以上で10%、50個以上で15%の割引。指定顧客の受注が1件もなければ合計0円。納品予定日の当日にまだ出荷されていない受注も遅延に数える。この3つを頭に入れてからテストを書きます。

置き場所には一手いります。このクラスは exercises/day2 にあり、Maven は src/main/java の下しかコンパイルしないため、テストクラスから参照できません。移さずに走らせると「シンボルを見つけられません」でコンパイルが止まります。移したあとは修正もそのファイルに対して行い、exercises 側には戻しません。

期待値は電卓で先に出しておきます。突き合わせる基準がないと、生成物の粗は見つけにくくなります。数量10・単価50000なら5%引きで475000円、いまの実装が返すのは500000円。getCustomerOrderTotal は合計の初期値が null のままなので、該当受注ゼロの顧客では null がそのまま返ります。メソッド自体は例外を投げません。NullPointerException になるのは戻り値を int で受けたときで、自動アンボクシングで null を数値に展開しようとして落ちます。期待値0で突き合わせるテストは値が合わずに Failures、int で受けるテストは Errors 側に出ます。

/gen-test はこのプロジェクトに同梱したコマンドで、実体は .claude/commands/gen-test.md です。handson フォルダを開いた状態で claude を起動すると、/ge まで打てば候補に出ます。テストが1件でも赤いと Maven は BUILD FAILURE で終わりますが、今日はそれが狙いどおりの状態なので、Results: の下の Tests run と Failures、Errors の行だけ読んでください。

修正後の確認は java -Dfile.encoding=UTF-8 src/main/java/com/example/order/buggy/BuggyOrderCalculator.java です。main に自己診断が入っていて、直る前は「バグ発生!」が3か所、直ったあとは割引と遅延が「正常」、存在しない会社の合計が0円と出ます。

D2-3コードスメールを見つけて全部潰す[35min]
ねらい

309行のレガシーコードから読みにくさの原因を自分で探し、出力を1文字も変えずに全部潰します。

この演習で扱うもの

扱うのは LegacyReportService という帳票出力のクラスです。309行あります。受注データから3種類のレポートを文字列で組み立てて返します。月次売上、顧客別サマリー、高額受注アラートの3つです。

データはクラスの中に8件持っています。受注番号、顧客名、商品名、数量、単価、ステータス、受注日。これを Map に詰めて、リストで持っています。外部とはつながっていないので、手元で何度実行しても同じ結果が出ます。整理の前後を比べるのに向いた題材です。

このクラスには、読みにくさの原因が8種類・のべ100箇所以上入っています。意図的に仕込んだものです。

なぜこれをやるのか

リファクタリングは、動きを1ミリも変えずに読みやすさだけを上げる作業です。普通の修正と目的が逆で、動きが変わったら失敗になります。

なぜ AI の時代にこれが要るのか。生成される量が増えるからです。自分で書いたコードなら中身を知っていますが、AI が書いたコードは知らないまま手元に残ります。その状態で読みにくいコードが積み上がると、次に直すときに誰も手を出せなくなります。

もう1つあります。読みにくいコードは AI にとっても読みにくい。変数が cnt や tmp のままだと、こちらの意図が伝わらず、見当違いの修正が返ってきます。整理しておくと、次に AI へ頼むときの精度が上がります。

ここまで出たら完了
  • 8種類のうち何種類を潰したか、種類の名前で言える
  • 潰すたびに diff を取り、そのすべてで何も表示されなかった
  • before.txt と after.txt の「合計金額(税込): 10,571,000円」が一致している
  • 潰せなかった種類について、なぜ手を出さなかったかを説明できる
完了時。diff が何も表示しないのが正解です
完了時。diff が何も表示しないのが正解です
さわるファイル
書くexercises/day2/LegacyReportService.java整理する対象。309行
新規before.txthandson 直下。整理前の出力
新規after.txthandson 直下。diff で比べる
まず自分で考える [5min]

ファイルを開いて、読みにくいと感じた箇所を探してください。AI はまだ使いません。全部見つける必要はありません。何種類見つけられるかを試してください。

「コードスメール」と呼ばれる、よくある形が8種類入っています。名前を知らなくても、読みにくいと感じたらそれがスメールです。行番号と、なぜ読みにくいかをメモしてください。

  • 同じような処理が何度も出てくる
  • 1つのメソッドが長すぎて、何をしているか一目で分からない
  • 数字がそのまま書かれていて、何の数字か分からない
  • 変数の名前から中身が想像できない
  • 括弧の入れ子が深くて、条件を追えない
AI と詰める

自分が見つけたものを AI に見せて、見落としを出させます。ここで初めて AI を使います。先に自分で探しておくと、返ってきた指摘のうちどれが自分の見落としかが分かります。いきなり AI に探させると、一覧を眺めて終わりになります。

返ってきたら、自分のメモと突き合わせてください。何種類見つけられたか、何を見落としたかが今日の収穫です。

@exercises/day2/LegacyReportService.java
このコードの読みにくい箇所を自分で探しました。

(自分が見つけたものを貼る)

他にコードスメールがあれば、種類の名前と行番号を挙げてください。
まだ直さないでください。一覧だけお願いします。
手を動かす
1
実行
潰す前に、いまの出力をファイルに落とします。これが安全網です。ターミナルで java -Dfile.encoding=UTF-8 exercises/day2/LegacyReportService.java > before.txt と打ちます。中を開いて「合計件数: 8件」「合計金額(税込): 10,571,000円」があることを確かめてください。
2
書く
確定した一覧から1種類ずつ潰します。1回の依頼で1種類だけです。「@exercises/day2/LegacyReportService.java の意味のない変数名を、分かる名前に変えてください。d.get("qty") のようなカッコの中の文字は絶対に変えないでください。処理の中身も変えないでください。」のように、やらないことを2つ添えます。
3
実行
1種類潰すたびに比べます。java -Dfile.encoding=UTF-8 exercises/day2/LegacyReportService.java > after.txt を打ち、diff before.txt after.txt で比較します。何も表示されなければ成功です。行が出たら、そこが壊した場所なので直前の変更に戻ります。
4
くり返す
潰す、比べる、を残り時間まで繰り返します。潰しやすい順は、変数名 → マジックナンバー → 文字列連結 → ネスト → 重複です。生の Map をクラスに置き換えるのが一番大きいので、残り20分を切ったら手を出さないでください。
考えること
出力が同じなら中身を読まなくてよい、と言えるのはなぜか。逆に、出力が同じでも安心できないのはどういうときか。
AI の出方
一度に「全部きれいにして」と頼むと、まず出力が変わります。特に文字列連結は87箇所あるので、まとめて StringBuilder に替えさせると改行やスペースがどこかで落ちます。1種類ずつ、できれば1メソッドずつです。 いちばん多い事故が、変数名を変えるときに d.get("qty") のカッコの中まで書き換えることです。これは変数ではなくデータの取り出しキーなので、変えると別のものを探しにいって実行時に落ちます。落ちたらエラーをそのまま貼って「カッコの中の文字は元に戻してください」で直ります。
D2-3+8種類のうち最後に残る Map<String, Object> を、OrderRecord というクラスに置き換えてください。影響範囲が3メソッド全部に及ぶので、Shift+Tab で Planモードに入り、計画を先に出させます。 順番が要ります。OrderRecord の定義と addOrder だけを先に通して diff を取り、そのあとレポートのメソッドを1つずつ移して、毎回 diff を取ります。まとめてやると、差が出たときに原因が3箇所に散ります。 ここで分かるのは、影響範囲が広い変更ほど段階を細かく切る必要がある、という点です。AI は一度に全部やろうとするので、段階を切るのは人の仕事になります。
くわしく(背景・詰まったときの対処)

仕込んであるコードスメールは次の8種類です。答え合わせに使ってください。

1. 長大メソッド。generateMonthlyReport が97行、generateCustomerSummary が91行、generateHighValueAlert が55行。集計と整形と出力を1つのメソッドでやっています。

2. ループ内の文字列連結。result = result + ... が87箇所。文字列は変更できない型なので、つなぐたびに新しい文字列が作られます。件数が増えると効いてきます。

3. マジックナンバー。税率の 1.1 が3箇所、高額の閾値 1000000 が1箇所、緊急の閾値 2000000 が2箇所。数字を見ても何の数字か分かりません。

4. 深いネスト。最大7階層。262行目から290行目あたりが特にひどく、条件を追うのが難しくなっています。

5. 重複したロジック。ステータスを判定する同じ形の分岐が84行目、179行目、261行目の3箇所にあります。1つ直したら3つとも直す必要があります。

6. 意味のない変数名。cnt、tmp、d、q、p、val、st、dt など12種類。名前から中身が分かりません。

7. 生の Map で持っている。Map<String, Object> が5箇所。取り出すたびにキャストが要り、キーを打ち間違えても気づけません。

8. 引数を使っていない。generateCustomerSummary は顧客名だけで絞り込んでいて、月の絞り込みがありません。

全部潰す必要はありません。35分で何種類まで行けるかです。1〜6は比較的安全で、7は影響範囲が広く、8は仕様の判断が要ります。8に手を出すと出力が変わるので、今日は触らないでください。

到達チェック

この日の終わりに、自分で確かめてください。全部埋まっていなくても構いません。

つまずいたとき

よく出る詰まりどころです。当てはまるものがなければ、その場で声をかけてください。

そもそもテストとは何ですか
コードで書いた確認手順のことです。品質チェックの担当者がやる検査とは別物です。中身は3つだけで、こういう値を渡したら(入力)、こういう結果になるはず(期待値)、違ったら知らせて(突き合わせ)。これを書いておくと、直すたびに何度でも同じ精度で確かめられます。手で画面を見るのは1回目だけなら速いのですが、2回目から逆転します。
赤が出ました。壊しましたか
赤は故障ではなく、期待した値と実際の値が違うという知らせです。原因は2つで、コードが違うか、期待値の書き方が違うかのどちらかです。今日は赤が2回出ます。1回目は配布した時点から出ている分で、わざと仕込んであります。2回目は D2-2 で自分から出す分です。直す前にわざと赤を出してから直します。
ターミナルが2枚あるのはなぜですか
1枚目はアプリを動かしっぱなしにするためです。2枚目で claude を起動したり mvn test を打ったりします。1枚で兼ねると、アプリを止めるつもりの Ctrl+C で Claude Code ごと落ちます。VSCode のターミナル右上のプラスボタンで2枚目が開きます。
claude を起動したままコマンドを打てますか
打てません。claude を起動していると入力欄が Claude Code のものになります。mvn test や java などを打つときは Ctrl+D か /exit で一度抜けてください。抜けても会話は残っているので、claude と打てば続きから再開できます。
Java を書いたことがありません。ついていけますか
大丈夫です。今日やることは、依頼を書いて、返ってきたものを見て、動かして確かめる、この3つです。Java の文法を覚える時間はありません。コードは読めなくて構いません。見るのは「どのファイルが変わったか」と「テストが緑になったか」だけです。COBOL でも Python でも C# でも、確認の仕方は同じです。
自分の担当言語で同じことができますか
できます。今日使う依頼の型(対象・やること・やらないこと・終わりの合図)に Java 固有のものは入っていません。テストを書かせる、失敗させてから直す、出力を固定して整理する、この3つの進め方も言語を選びません。演習の題材が Java なだけです。
終わったかどうか分からなくなります
各演習の先頭に「ここまで出たら完了」を置いています。手順より先にそこを読んでください。画面に何が出れば終わりかを最初に見ておくと、途中で迷いません。前回ここで止まったので、今回から順番を変えました。
ふつうの日本語で頼むだけでいいのですか。もっと決まった書き方はありませんか
ふつうの日本語で構いません。ただし4つを入れてください。どのファイルの話か、何ができればいいか、やってほしくないこと、どうなったら終わりか。この4つが揃っていれば、書き方は自由です。前回うまくいった依頼にはこの4つが入っていて、やり直しになった依頼には欠けていました。長く書くことと精度は関係ありません。
コマンドをどこに打つのか分かりません
VSCode の統合ターミナルで handson フォルダを開いた状態で打ちます。Day1 から mvn spring-boot:run を動かしたままでも構いません。その場合はターミナルの + ボタンで2枚目を開き、そちらで打ちます。mvn も java も、このフォルダで打たないと動きません。claude を起動しているあいだは入力欄が Claude Code のものになるので、ターミナルのコマンドを打つ前に Ctrl+D か /exit で一度抜けてください。逆に /gen-test のようなスラッシュコマンドは、claude を起動した中で打ちます。
BUILD FAILURE と出ます
テストが1件でも赤いと Maven はビルド失敗として終わります。今日は赤を先に出すのが目的なので、Results: の下の Tests run と Failures、Errors の行だけ読んでください。BUILD FAILURE の文字自体は異常ではありません。
生成されたコードが正しいか判断できません
最初に期待値だけ見てください。仕様の値ではなく、いまの実装が返す値で書かれていたら、そのテストはバグごと固定します。ここを外すと後の判断が全部ずれます。ほかは、assert が値を比較しているか、異常系が例外の型まで指定されているか。Java の細部を追わなくても差分の見た目で分かります。判断がつかないところは採用せず、条件を足して出し直せば済みます。
配布時点で mvn test が赤いです
DELIVERED の受注をキャンセルできてしまう穴が cancelOrder に残してあります。mvn test では10件中1件(OrderApplicationTests の1件を含む)、mvn test -Dtest=OrderServiceImplTest なら9件中1件が赤い状態が正常です。Day3 で扱うので、今日は直さずそのままにしてください。環境が動くかどうかだけ確かめたいときは mvn compile を打ちます。テストを走らせないので、この赤に邪魔されずに BUILD SUCCESS が出ます。clean は付けません。アプリを起動したままだと target の削除に失敗することがあります。
BuggyOrderCalculatorTest がコンパイルで止まります
「シンボルを見つけられません」が出ているはずです。Maven は src/main/java の下しかコンパイルしないので、exercises/day2 に置いたままのクラスはテストから見えません。src/main/java/com/example/order/buggy を作って移してから走らせてください。
diff で出力日の行だけ差が出ます
出力日は実行日で変わるので、日をまたいで比べたときはその行だけ差が出ます。ほかの行に差がなければ振る舞いは守れています。同じ日のうちに比べれば何も表示されません。
テスト名の日本語が ? の羅列になります
事前セットアップの Step 6(文字コードの設定)を飛ばしています。テストの合否は正しく出ているので、そのまま進めても演習は成立します。読めるようにするには、ターミナルで locale charmap を打って UTF-8 が返るか確かめてください。ANSI_X3.4-1968 と返る場合は echo 'export LANG=C.UTF-8' >> ~/.bashrc のあと source ~/.bashrc で直ります。落ちているのは OrderServiceImplTest$CancelOrderTest の1件です。