はじめに
この記事を書くにあたって、少し経緯をお話します。
みなさんは「単体テスト」の意味について、改めて考えたことはありますか?
単体というからには、対象のクラスだけをテストする必要があるはずです。(プロジェクトによって粒度はさまざまかもしれませんが…)
しかし、何も意識せずに研修などで学んだ知識をもとにテストを書こうとすると、つい@Autowired
でServiceを注入してしまうことがあるのではないでしょうか。
これだと、本来の目的である「Controllerの振る舞いの検証」が曖昧になってしまいます。
この記事では、ControllerのテストにおいてServiceをMock化する理由とそのメリットについて整理します。
Controllerの責務とは?
Controllerは主に以下の役割を担います:
- リクエストの受け取り
- パラメータのバリデーション
- Serviceへの処理委譲
- レスポンスの返却
つまり、Controllerはアプリケーションの入り口であり、ビジネスロジックはService層に委ねる設計が一般的です。
なぜServiceをMockするのか?
Controllerのテストでは、Controller単体の振る舞いを検証することが目的です。
Serviceを実装ごと注入してしまうと、以下のような問題が発生します:
- Service内部の例外が原因でテストが失敗した場合、Controllerの問題なのかServiceの問題なのかが判別できない
- Controllerのテストなのに、Serviceの実装に依存したテスト結果になってしまう
- DBアクセスなどが含まれると、テストの実行速度や安定性が低下する
Mock化のメリット
- テストの対象が明確になる(Controllerの振る舞いのみを検証)
- 異常系のテストが容易になる(Serviceが例外を返すケースなど)
- テストの実行が高速かつ安定(外部依存がない)
実装例(Spring Boot + Mockito)
@SpringBootTest
public class SampleControllerTest {
@Autowired
private SampleController controller;
@MockBean
private SampleService sampleService;
@Nested
DisplayName("getSamples関数のテスト")
class GetSamplesTest {
@Test
@DisplayName("[正常系] 条件付きでレスポンスを取得")
void testGetSamplesSuccess() {
// 入力値
Optional<Integer> filter = Optional.of(100);
// モックの戻り値
List<SampleResponse> mockResult = new ArrayList<>();
mockResult.add(new SampleResponse(1, "データA"));
mockResult.add(new SampleResponse(2, "データB"));
// モック設定(この部分がポイント!下で詳しく説明します)
when(sampleService.getSamples(filter)).thenReturn(mockResult);
// 実行
List<SampleResponse> result = controller.getSamples(filter);
// 検証
assertEquals(1, result.get(0).getId());
assertEquals("データA", result.get(0).getName());
assertEquals(2, result.get(1).getId());
assertEquals("データB", result.get(1).getName());
}
Point
when(sampleService.getSamples(filter)).thenReturn(mockResult)
これはつまり
sampleService.getSamples(filter)
が呼び出されたときに、
モックとして(テスト用に) mockResult
を返すように定義している
だけなのです!
まとめ
Controllerのテストでは、Service層をMock化することで、テストの粒度と目的を明確に保つことができる。
これは保守性の高いテストコードを書く上での基本的な設計方針であり、テストの信頼性にも直結する。