Claude CodeでJUnit 5とMockitoのテストを書く — Spring Bootのテストスライスの選び方
Spring BootのテストをClaude Codeに書かせるとき、@WebMvcTest・@DataJpaTest・@SpringBootTestをどう指定し、モックの入れすぎをどう防ぐか。依頼文とCLAUDE.mdの書き方を示します。
Spring Bootのテストは、アノテーション1つで「どこまでアプリを起動するか」が決まります。Claude Codeに「テストを書いて」とだけ頼むと、この選択が曖昧なまま、全体を起動する重いテストか、モックだらけで何も確かめないテストのどちらかに寄りがちです。
そこで依頼文の側で、スライス(アプリの一部だけを立ち上げる仕組み)を名指しします。あわせて、モックを置いてよい境界をCLAUDE.mdに書きます。この記事はその2点を、JUnit 5とMockitoのコード付きで順に扱います。
テストスライスとは何か — どこまで起動するかの選択
テストスライスは、Spring Bootのテスト用アノテーションが読み込む範囲を絞る仕組みです。Spring Bootのテストガイドによると、各スライスはコンポーネントスキャンを該当部分に限り、ごく限られた自動設定だけを読み込みます。Web層だけ、JPA層だけ、JSONの変換だけといった単位で、テストの起動が軽くなります。
| 検証したいこと | アノテーション | 読み込まれる範囲 |
|---|---|---|
| コントローラーの入出力 | アノテーション@WebMvcTest | 読み込まれる範囲@Controller、@ControllerAdvice、Filterなど。MockMvcも自動設定 |
| リポジトリのクエリ・マッピング | アノテーション@DataJpaTest | 読み込まれる範囲@EntityとSpring Data JPAのリポジトリ。組み込みDBがあれば自動設定 |
| JSONの変換 | アノテーション@JsonTest | 読み込まれる範囲JSONマッパーの自動設定 |
| 層をまたぐ流れ | アノテーション@SpringBootTest | 読み込まれる範囲アプリ全体。既定ではサーバーを起動しない |
| 1クラスのロジック | アノテーションなし(JUnit 5 + Mockito) | 読み込まれる範囲Springを起動しない |
最後の行はスライスではなく、Springを使わない普通の単体テストです。表の上から順に起動の範囲が広がり、1本あたりの実行も重くなります。
@SpringBootTestのwebEnvironmentも選択肢です。既定はMOCKで、実サーバーは立ち上がりません。RANDOM_PORTにすると組み込みサーバーがランダムなポートで起動し、NONEにするとWeb環境そのものを作りません。
複数のスライスを1つのテストに重ねることはできません。2つ必要なら、片方の@…Testを選び、もう片方の@AutoConfigure…を手で足します。Claude Codeが2つの@…Testを並べたコードを返したときは、この点を指摘して直させます。
依頼文でスライスを指定する
「Spring Bootのテストを書いて」では、Claude Codeはテスト対象の層から推測するしかありません。公式のワークフロー集も、テストを頼むときは検証したい挙動を具体的に書くよう勧めています。Springのテストなら、検証したい層とアノテーションの組を依頼文に入れるのが近道です。
UserControllerのGET /users/{id}をテストして。
@WebMvcTestを使い、UserServiceは@MockitoBeanで差し替える。
200のときのJSONと、存在しないIDで404になるときの2本を書いて。UserRepositoryのfindByEmailを@DataJpaTestでテストして。
リポジトリはモックにせず、実際にpersistして検索する。OrderServiceのキャンセル処理をJUnit 5とMockitoの単体テストで書いて。
Springは起動しない。PaymentGatewayだけモックにする。どの依頼にも、使うアノテーションと「何をモックにして何をモックにしないか」が入っています。この2点が決まれば、Claude Codeが迷う余地はほとんど残りません。
層の選び方を切り分ける
依頼文を書く前に迷うなら、上から順に答えると選べます。
どのテストを依頼するかの切り分け
- QWeb層からDBまで通した確認は不要ですか?答えがいいえ(Web層からDBまで通したい)答えがはい
@SpringBootTestを指定します。本数は絞り、主要な経路だけにします。 - Qクエリやエンティティのマッピングの確認は不要ですか?答えがいいえ(クエリを見たい)答えがはい
@DataJpaTestを指定します。リポジトリはモックにせず、実際に読み書きさせます。 - QHTTPの入出力の確認は不要ですか?答えがいいえ(HTTPの入出力を見たい)答えがはい
@WebMvcTestを指定します。協力クラスは@MockitoBeanで差し替えます。
JUnit 5 + Mockitoの単体テスト
Springを起動せず、外部に出ていく依存だけをモックにします。
@WebMvcTestでコントローラーを確かめる
@WebMvcTestは、Spring MVCの基盤を組み立てます。走査されるのは@Controller、@ControllerAdvice、Filterなどで、通常の@Componentと@ConfigurationPropertiesは走査されません。コントローラーが使うサービスは、テスト側で用意する必要があります。
そこで使うのが@MockitoBeanです。Spring Frameworkが提供するアノテーションで、ApplicationContext内のBeanをMockitoのモックに差し替えます。テストガイドも、@WebMvcTestは単一のコントローラーに限り、必要な協力クラスを@MockitoBeanで補う使い方が多いと説明しています。公式の例に沿った形は、次のとおりです。
@WebMvcTest(UserController.class)
class UserControllerTests {
@Autowired
private MockMvcTester mvc;
@MockitoBean
private UserService userService;
@Test
void 存在するIDなら名前を返す() {
given(userService.findName(1L)).willReturn("Alice");
assertThat(mvc.get().uri("/users/1"))
.hasStatusOk()
.hasBodyTextEqualTo("Alice");
}
}ここでのモックは、コントローラーの「外」にあるサービスだけです。確かめたいのは、URLのマッピング、ステータスコード、レスポンスの本文であり、サービスの中身ではありません。モックの置き場所が検証対象の境界と一致している点が、後述する「入れすぎ」との違いです。
MockMvcにはHamcrest版のほか、AssertJで書けるMockMvcTesterもあります。AssertJが使える環境ならMockMvcTesterが自動設定されるため、既存テストの書き方を見てそろえるよう依頼に添えます。
@DataJpaTestでは実際に読み書きする
@DataJpaTestは@Entityを走査し、Spring Data JPAのリポジトリを設定します。組み込みDBがクラスパスにあれば、それも用意されます。テストは既定でトランザクション内で動き、終わるとロールバックされます。
@DataJpaTest
class UserRepositoryTests {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void メールアドレスで検索できる() {
entityManager.persist(new User("alice@example.com"));
User found = repository.findByEmail("alice@example.com");
assertThat(found.getEmail()).isEqualTo("alice@example.com");
}
}リポジトリの検索メソッドは、メソッド名からクエリが組み立てられるものも、@Queryで書かれるものもあります。どちらも、実際にDBへ投げて初めて正しさが分かります。リポジトリをモックにした@WebMvcTestや単体テストでは、クエリの誤りは見つかりません。DB周りの検証は@DataJpaTestに寄せ、そこでモックは使わない、と依頼文で固定すると安定します。
組み込みDBでは足りず、本番と同じDBで確かめたいときは、@AutoConfigureTestDatabase(replace = Replace.NONE)を併用します。どちらにするかは、使っている方言固有のSQLがあるかで決まります。
モックを入れすぎたテストを避ける
Claude Codeは、通るテストを素早く書けます。ただし、通るだけのテストと、不具合を見つけるテストは別物です。モックが多すぎると、前者になります。
次のテストは、通るのに何も確かめていません。
@ExtendWith(MockitoExtension.class)
class OrderServiceTests {
@Mock
private OrderRepository repository;
@InjectMocks
private OrderService service;
@Test
void 注文を取得する() {
Order order = new Order(1L, "NEW");
when(repository.findById(1L)).thenReturn(Optional.of(order));
Order result = service.find(1L);
assertThat(result).isSameAs(order);
verify(repository).findById(1L);
}
}OrderService.findがrepository.findByIdを呼んで結果を返すだけの実装なら、このテストは実装をそのまま書き写したものです。サービスにバグがあってもモックが返した値を返す限り、テストは緑のままです。
直し方は、モックを置く理由で振り分けることです。
- 外部のHTTP APIや決済のように、テストで本物を呼べない依存は、モックにする
- 自分のDBやリポジトリは、
@DataJpaTestで本物を使う - 検証対象のクラス自身と、その内部のヘルパーは、モックにしない
verifyで呼び出し回数を縛るのは、「呼ばれること」自体が仕様のとき(通知の送信など)に限る
この方針を、Claude Codeの依頼のたびに書くのは手間です。次節のCLAUDE.mdに残します。
CLAUDE.mdに書くテスト方針
公式のベストプラクティスによると、CLAUDE.mdには、コードから読み取れないこと、とくにテストの実行コマンドと、守ってほしい規約を書きます。短く保つほど効きます。Spring Bootのテストなら、次のような内容になります。
# Commands
- Test all: ./gradlew test
- Test one class: ./gradlew test --tests com.example.UserControllerTests
# Test conventions
- JUnit 5 + Mockito + AssertJ。新しいテストは既存テストの書き方に合わせる
- コントローラー: @WebMvcTest + @MockitoBean(協力クラスのみ)
- リポジトリ: @DataJpaTest。リポジトリをモックにしない
- @SpringBootTest は層をまたぐ主要経路だけ。1機能につき1本まで
- モックにしてよいのは外部HTTP・決済・メール送信。検証対象クラス自身はモックにしない
- verify() は「呼ばれること」が仕様のときだけコマンド名はMavenなら./mvnw testに置き換えます。1クラス単位の実行コマンドを書いておくと、Claude Codeは変更のたびに全体を回さず、書いたテストだけを再実行できます。
ここに書いた規約は、守られることが大半ですが、毎回守られる保証はありません。ベストプラクティスも、CLAUDE.mdの一行は「たいていは効くが毎回ではない」と述べています。必ず通したい条件は、後述のとおり、実行結果で確認します。
実行結果を証拠にして反復する
テストを書かせた後は、実行コマンドを許可しておくと反復が止まりません。.claude/settings.jsonに許可ルールを置きます。
{
"permissions": {
"allow": [
"Bash(./gradlew test *)"
]
}
}権限ルールでは、末尾の*の前にスペースを置くと、引数なしの./gradlew testにもマッチします。--tests付きの単一クラス実行も同じルールで通ります。
次に、依頼文に完了の条件を書きます。
UserControllerTestsを書いたら ./gradlew test --tests com.example.UserControllerTests を実行して。
全部通るまで直して。最後に実行したコマンドと出力の末尾を見せて。ベストプラクティスは、完了を「見た目」でなく「実行できる合否」に置くこと、そして「全部通りました」の一言でなく、実行したコマンドとその出力を求めることを勧めています。
もう1つ、緑のテストが本当に何かを守っているかを確かめる手があります。実装をわざと壊し、そのテストが赤くなるかを見るのです。
OrderService.findの戻り値をnullに変えて、OrderServiceTestsが失敗することを確認して。
確認したら元に戻して。赤くならないテストは、モックの返値を検証しているだけの可能性が高いです。この確認はClaude Codeの機能ではなく、テストの質を測る運用上の工夫です。
古いSpring Bootで気をつけること
テストガイドの最新版は、新しいパッケージ構成で書かれています。たとえば@WebMvcTestのインポートはorg.springframework.boot.webmvc.test.autoconfigure、@DataJpaTestはorg.springframework.boot.data.jpa.test.autoconfigureです。@WebMvcTestの属するモジュールはspring-boot-webmvc-testと説明されています。
古いSpring Bootのプロジェクトでは、インポートのパッケージ名や依存の名前が異なる場合があります。Claude Codeが最新版の書き方を写してコンパイルエラーになったときは、まずビルドファイルのSpring Bootのバージョンを確認させます。依頼文の冒頭に「ビルドファイルのSpring Bootバージョンに合うインポートで書く」と添えるのが確実です。
JUnitのバージョンにも注意点があります。JUnit 4を使う場合は@RunWith(SpringRunner.class)を足さないと、Spring Bootのテスト用アノテーションが無視されます。JUnitのバージョンごとの追加設定は、使っているSpring Bootのテストガイドの該当箇所で確認します。
よくあるつまずき
スライスで「Beanが見つからない」と言われる
@WebMvcTestと@DataJpaTestは通常の@Componentを走査しません。コントローラーのテストでサービスが見つからないときは、@MockitoBeanで補うか、@Importで必要な設定を取り込みます。@SpringBootTestに変えて解決するのは、軽さを手放す選択です。
落とし穴
@SpringBootTestに戻すと、テストが全部重くなる
Beanが足りないエラーの対処として、アノテーションを@SpringBootTestに差し替える案がよく出ます。通るようになりますが、アプリ全体の起動が1本ごとに走る構成になり、本数が増えるほど実行時間が伸びます。まず足りないBeanを@MockitoBeanか@Importで補えないかを確認します。
RANDOM_PORTのテストでロールバックされない
@SpringBootTestのテストが@Transactionalでも、RANDOM_PORTかDEFINED_PORTの場合、HTTPクライアントとサーバーは別スレッド、つまり別トランザクションで動きます。サーバー側で始まったトランザクションはロールバックされません。テストデータの後始末を、別の手段で用意する必要があります。
複数のスライスを重ねてしまう
@WebMvcTestと@DataJpaTestを同じクラスに付けるコードは、サポートされません。Web層とDB層を両方通したいなら、@SpringBootTestに@AutoConfigureMockMvcを足す形に切り替えます。
まとめ
Spring Bootのテストの質は、スライスの選択とモックの置き場所でほぼ決まります。Claude Codeには、層の名前とアノテーション、モックにするものとしないものを、依頼文とCLAUDE.mdで先に渡します。書かせた後は、実行結果を証拠にして反復し、実装をわざと壊して赤くなることまで見ておくと、通るだけのテストを減らせます。
Spring Bootアプリそのものの作り方はClaude CodeでSpring Bootアプリを作るに、完了条件でテストを回す考え方はPowerShellのPesterテストとBlazorのbUnitテストの記事に同じ型があります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。