Spring Bootでテストを書いていると、さまざまな用語やツールが登場します。
JUnit
Mockito
MockMvc
@SpringBootTest
@WebMvcTest
@DataJpaTest
最初に学習したとき、
結局どれが何をテストしているのか?
という部分がかなり分かりづらく感じました。
そこでこの記事では、細かい書き方に入る前に、Spring Bootにおけるテストの全体像を整理します。
まずSpring Bootアプリの構造を確認する
一般的なSpring Bootアプリケーションでは、次のように役割を分けます。
HTTP Request
|
v
+------------+
| Controller |
+------------+
|
v
+------------+
| Service |
+------------+
|
v
+------------+
| Repository |
+------------+
|
v
+------------+
| Database |
+------------+
ざっくり言えば、
| 層 | 主な役割 |
|---|---|
| Controller | HTTPリクエストを受け取る |
| Service | ビジネスロジックを実行する |
| Repository | DBへアクセスする |
| Database | データを保存する |
となります。
テストでは、
このうち、どこまでをまとめて動かすのか?
という考え方が重要になります。
単体テストとは
単体テスト(Unit Test)は、クラスやメソッドなどの小さな単位を独立してテストするものです。
例えばServiceをテストするとします。
Controller
|
X
+-----------+
| Service | ← ここをテスト
+-----------+
|
v
Repository
ServiceはRepositoryを利用します。
しかしServiceだけをテストしたいのに、本物のDBまで必要になるとテストが大きくなってしまいます。
そこでRepositoryを**Mock(モック)**に置き換えます。
+-----------+
| Service | ← 本物
+-----------+
|
v
+-----------+
| Mock Repo | ← 偽物
+-----------+
これによって、
Repositoryがこの値を返した場合、Serviceは正しく処理できるか?
をServiceだけに絞って確認できます。
Mockとは
Mockは、テストで本物の依存オブジェクトの代わりとして利用するオブジェクトです。
例えば本物のRepositoryなら、
Service
|
v
Repository
|
v
Database
となります。
Mockを使うと、
Service
|
v
Mock Repository
となります。
そして、
when(repository.findById(1L))
.thenReturn(Optional.of(equipment));
のように、
findById(1L)が呼ばれたら、このEquipmentを返してね
と振る舞いを決められます。
JUnitとは
JUnitは、Javaでテストを書くためのテスティングフレームワークです。
例えば、
@Test
void addすると2つの数値を加算できる() {
int result = calculator.add(10, 20);
assertEquals(30, result);
}
のようなテストを書けます。
JUnitは、
@Test
テストの実行
アサーション
テストのライフサイクル
など、Javaでテストを書くための基本的な仕組みを提供します。
つまり、
JUnit = Javaでテストを書くための土台
と考えると分かりやすいです。
Mockitoとは
Mockitoは、JavaでMockを作成するためのライブラリです。
例えば、
@Mock
private EquipmentRepository equipmentRepository;
とすると、RepositoryのMockを作成できます。
さらに、
when(equipmentRepository.findAll())
.thenReturn(List.of(equipment));
のようにMockの振る舞いを設定できます。
つまり、
JUnit
↓
テストを書く・実行する
Mockito
↓
テストに必要なMockを作る
という役割の違いがあります。
JUnitとMockitoは競合するものではなく、一緒に利用することが多いです。
Controllerを直接呼ぶ単体テスト
Controllerも普通のJavaクラスなので、直接メソッドを呼び出してテストできます。
例えば、
DashboardController controller =
new DashboardController(mockService);
controller.dashboard("pump", model);
のような形です。
この場合、
HTTP
X
Controller ← 直接呼び出す
|
v
Mock Service
となります。
Controller自身のロジックを素早く確認できます。
ただし、実際のHTTPリクエストは通していません。
そのため、
GET /dashboard
というHTTPリクエストとして正しく処理できるかまでは確認できません。
MockMvcとは
MockMvcは、Spring MVCのWeb層をテストするための仕組みです。
例えば、
mockMvc.perform(get("/dashboard"))
.andExpect(status().isOk())
.andExpect(view().name("dashboard"));
のように書けます。
イメージとしては、
疑似HTTP Request
|
v
Spring MVC
|
v
Controller
です。
Controllerのメソッドを直接呼び出すのではなく、
GET /dashboard
というHTTPリクエストに近い形でテストできます。
そのため、
- URL
- HTTPメソッド
- ステータスコード
- View名
- Model
- バリデーション
など、Web層の振る舞いを確認できます。
@WebMvcTestとは
@WebMvcTestは、主にSpring MVCのWeb層に絞ってテストするときに利用します。
例えば、
@WebMvcTest(DashboardController.class)
class DashboardControllerTest {
}
とします。
イメージとしては、
Spring Boot Application
+-------------------+
| Controller | ← 起動
| Spring MVC | ← 起動
+-------------------+
Service ← Mockなどにする
Repository ← 基本対象外
Database ← 基本対象外
となります。
アプリケーション全体を起動するより対象を絞れるため、Controller周辺のテストに向いています。
Repositoryのテスト
Repositoryでは、実際にDBとのやり取りが正しくできるかを確認したい場合があります。
例えば、
List<Equipment> result =
repository.findByNameContainingIgnoreCase("pump");
という検索が正しく動くか確認するとします。
この場合、RepositoryをMockにしてしまうと意味がありません。
Repository ← 本物
|
v
Database ← テスト用DB
として確認する必要があります。
@DataJpaTestとは
JPA Repositoryのテストで利用できるのが@DataJpaTestです。
@DataJpaTest
class EquipmentRepositoryTest {
}
JPA関連のコンポーネントを中心に読み込み、RepositoryとDBの連携をテストできます。
例えば、
EquipmentRepository
|
v
JPA
|
v
Test Database
という範囲を確認できます。
ServiceやControllerまで含める必要はありません。
@SpringBootTestとは
@SpringBootTestは、Spring BootのApplicationContextを広い範囲で読み込んでテストします。
イメージとしては、
Controller
|
v
Service
|
v
Repository
|
v
Database
をまとめて動かすテストに利用できます。
例えば、
@SpringBootTest
class EquipmentApplicationTest {
}
のように使います。
対象範囲が広い分、
単体テスト
↓
速い・対象が小さい
@SpringBootTest
↓
比較的重い・対象が広い
という違いがあります。
結合テストとは
結合テスト(Integration Test)は、複数のコンポーネントを組み合わせて正しく動くか確認するテストです。
例えば、
Controller
|
v
Service
|
v
Repository
を組み合わせて確認します。
単体では正しく動いていても、
Controller → Service
Service → Repository
の接続部分に問題がある可能性があります。
そのため、単体テストとは別に結合した状態でも確認します。
E2Eテストとは
E2EはEnd-to-Endの略です。
ユーザーが実際に利用する流れに近い形で、システム全体を確認します。
例えば、
Browser
|
v
HTTP
|
v
Spring Boot
|
v
Database
まで通します。
例えば設備管理システムなら、
ログイン
↓
設備一覧を表示
↓
設備を検索
↓
点検結果を登録
↓
登録結果を確認
といった一連の操作をテストできます。
対象範囲が広い分、実行時間やテスト環境の準備も大きくなります。
テストはピラミッドで考える
テストには「テストピラミッド」という考え方があります。
/\
/ \
/ E2E\
/------\
/ 結合 \
/----------\
/ 単体 \
/______________\
一般的には、
単体テスト
→ 多い・速い
結合テスト
→ 中くらい
E2E
→ 少ない・重い
というバランスを取ります。
すべてをE2Eで確認すると、テストの実行に時間がかかります。
逆に単体テストだけでは、コンポーネント同士を接続したときの問題を見つけられません。
そのため、それぞれを組み合わせます。
Spring Bootのテストを整理すると
ここまでをまとめると、ざっくり次のようになります。
| ツール・アノテーション | 主な役割 |
|---|---|
| JUnit | Javaでテストを書くための基本フレームワーク |
| Mockito | Mockを作成する |
| MockMvc | Spring MVCのWeb層をHTTPに近い形でテストする |
@WebMvcTest |
Web層を中心にテストする |
@DataJpaTest |
JPA / Repositoryを中心にテストする |
@SpringBootTest |
Spring Bootの広い範囲を読み込んでテストする |
そして、アプリケーションの各層に当てはめると、
HTTP
|
v
Controller
| ↑
| @WebMvcTest + MockMvc
|
v
Service
| ↑
| JUnit + Mockito
|
v
Repository
| ↑
| @DataJpaTest
|
v
Database
----------------------------
全部まとめて確認
↑
@SpringBootTestなど
というイメージになります。
何をテストしたいかを先に考える
Spring Bootのテストでは、
とりあえず@SpringBootTestを付ければいい
というわけではありません。
まず、
何を確認したいのか
を考える必要があります。
例えば、
Serviceの計算ロジックだけ確認したい
↓
JUnit + Mockito
ControllerのURLやViewを確認したい
↓
MockMvc + @WebMvcTest
Repositoryの検索処理を確認したい
↓
@DataJpaTest
複数の層を接続して確認したい
↓
結合テスト
というように、目的によってテスト方法を選びます。
まとめ
Spring Bootではテスト関連の仕組みが多いため、最初は混乱しやすいですが、
「どの範囲をテストしているのか」
で整理すると理解しやすくなります。
小さい
Service
↓
JUnit + Mockito
Controller
↓
@WebMvcTest + MockMvc
Repository
↓
@DataJpaTest
複数の層
↓
@SpringBootTestなど
E2E
↓
システム全体
大きい
JUnit、Mockito、MockMvcなどは同じ役割のツールではありません。
それぞれ、
JUnit = テストの土台
Mockito = 依存先をMockにする
MockMvc = Web層をHTTPに近い形でテストする
という役割があります。
まず「何をテストしたいのか」を決めてから、その範囲に合ったテスト方法を選択することが重要です。