Spring BootでServiceの単体テストを書くと、JUnitとMockitoを組み合わせることがよくあります。
例えば次のようなコードです。
@Mock
private EquipmentRepository equipmentRepository;
@InjectMocks
private EquipmentService equipmentService;
@Test
void searchで検索結果を取得できる() {
when(equipmentRepository
.findByNameContainingIgnoreCase("pump"))
.thenReturn(List.of(equipment));
List<Equipment> result =
equipmentService.search("pump");
assertEquals(1, result.size());
verify(equipmentRepository)
.findByNameContainingIgnoreCase("pump");
}
最初に見ると、
@Mock
@InjectMocks
when
thenReturn
verify
と知らないものが大量に登場します。
この記事では、Serviceの単体テストを題材に、
JUnitとMockitoがそれぞれ何をしているのか
を整理します。
今回テストする構成
Spring BootではServiceからRepositoryを呼び出すことがあります。
Controller
|
v
Service
|
v
Repository
|
v
Database
今回はServiceだけをテストしたいので、
Controller
X
+-----------+
| Service | ← テスト対象
+-----------+
|
v
+-----------------+
| Mock Repository | ← 偽物
+-----------------+
Database
X
とします。
つまり、
Serviceは本物、RepositoryはMock
です。
なぜRepositoryをMockにするのか
例えば次のServiceがあるとします。
@Service
public class EquipmentService {
private final EquipmentRepository equipmentRepository;
public EquipmentService(
EquipmentRepository equipmentRepository) {
this.equipmentRepository = equipmentRepository;
}
public List<Equipment> findAll() {
return equipmentRepository.findAll();
}
}
Serviceをテストしたいだけなのに本物のRepositoryを使うと、
Service
↓
Repository
↓
JPA
↓
Database
まで必要になります。
すると、
Serviceのテスト
だったはずが、DBまで含む大きなテストになります。
そこでRepositoryをMockにします。
Service
↓
Mock Repository
これによってServiceだけに集中できます。
JUnitとMockitoの役割
まず両者の役割を整理します。
JUnit
JUnitはJavaのテスティングフレームワークです。
例えば、
@Test
void test() {
int result = 1 + 1;
assertEquals(2, result);
}
のように、
@Test
テスト実行
assertEquals()
などを提供します。
Mockito
MockitoはMockを作成するためのライブラリです。
例えば、
@Mock
private EquipmentRepository equipmentRepository;
としてRepositoryのMockを作れます。
さらに、
when(equipmentRepository.findAll())
.thenReturn(List.of(equipment));
として、
このメソッドが呼ばれたら、この値を返してください
という振る舞いを設定できます。
MockitoをJUnitで使えるようにする
JUnit 5でMockitoを利用する場合、例えば次のようにします。
@ExtendWith(MockitoExtension.class)
class EquipmentServiceTest {
}
MockitoExtensionによって、
@Mock
@InjectMocks
などのMockitoアノテーションをJUnit 5のテストで利用できます。
@Mockとは
@MockはMockオブジェクトを作成します。
@Mock
private EquipmentRepository equipmentRepository;
これは本物のRepositoryではありません。
EquipmentRepository
本物
↓
Databaseへアクセスする
Mock
↓
テストで決めた振る舞いをする
例えば、
when(equipmentRepository.findAll())
.thenReturn(List.of(equipment));
と設定できます。
すると、
equipmentRepository.findAll();
が呼ばれたとき、本物のDBへアクセスせず、
List.of(equipment)
を返します。
@InjectMocksとは
次に、
@InjectMocks
private EquipmentService equipmentService;
です。
これはテスト対象となるEquipmentServiceを作成し、Mockitoが利用可能なMockを依存関係として注入します。
今回なら、
EquipmentService
|
v
EquipmentRepository
という依存関係があります。
Repositoryには、
@Mock
private EquipmentRepository equipmentRepository;
があります。
そのため、
EquipmentService
|
v
Mock EquipmentRepository
という状態を作れます。
when().thenReturn()とは
Mockitoで非常によく使うのが、
when(...).thenReturn(...);
です。
例えば、
when(equipmentRepository.findAll())
.thenReturn(List.of(equipment));
なら、
equipmentRepository.findAll()
が呼ばれたら
↓
List.of(equipment)
を返す
という意味です。
これを**Stub(スタブ)**として振る舞いを設定する、と表現することもあります。
Arrange・Act・Assertで考える
テストコードは、
Arrange / Act / Assert
で分けて考えると理解しやすくなります。
例えば、
@Test
void findAllで設備一覧を取得できる() {
// Arrange
Equipment equipment = new Equipment();
when(equipmentRepository.findAll())
.thenReturn(List.of(equipment));
// Act
List<Equipment> result =
equipmentService.findAll();
// Assert
assertEquals(1, result.size());
}
それぞれ、
Arrange
↓
テストの準備
Act
↓
テスト対象を実行
Assert
↓
結果を確認
です。
Arrange
まずテストに必要な状態を準備します。
Equipment equipment = new Equipment();
when(equipmentRepository.findAll())
.thenReturn(List.of(equipment));
ここでは、
Repositoryが設備を1件返す
という状況を作っています。
Act
次にテスト対象を実行します。
List<Equipment> result =
equipmentService.findAll();
重要なのは、
Mockを直接テストしているわけではない
ということです。
実行するのは本物のServiceです。
Test
|
v
EquipmentService ← 本物
|
v
EquipmentRepository ← Mock
となっています。
Assert
最後に結果を確認します。
assertEquals(1, result.size());
これは、
期待値
1
実際の値
result.size()
が同じか確認しています。
異なっていればテストは失敗します。
verify()とは
Mockitoにはverify()があります。
例えば、
verify(equipmentRepository).findAll();
です。
これは、
equipmentRepository.findAll()が実際に呼ばれたか?
を確認しています。
つまり、
assertEquals()
と
verify()
では確認しているものが違います。
assertとverifyの違い
assertは結果を確認します。
assertEquals(1, result.size());
つまり、
Serviceを実行した結果
↓
正しい?
を確認しています。
一方verify()は、Mockとのやり取りを確認します。
verify(equipmentRepository).findAll();
つまり、
Service
↓
Repository.findAll()
本当に呼んだ?
を確認しています。
整理すると、
| 方法 | 確認するもの |
|---|---|
assertEquals() |
実行結果 |
verify() |
Mockとの呼び出し |
となります。
verify()は回数も確認できる
例えば、
verify(equipmentRepository, times(1))
.findAll();
とすると、
findAll()が1回呼ばれた
ことを確認できます。
逆に、
verify(equipmentRepository, never())
.deleteAll();
なら、
deleteAll()が一度も呼ばれていない
ことを確認できます。
検索Serviceをテストしてみる
もう少し実際のSpring Bootアプリらしい例を考えます。
例えば、
public List<Equipment> search(String keyword) {
if (keyword == null || keyword.isBlank()) {
return equipmentRepository.findAll();
}
return equipmentRepository
.findByNameContainingIgnoreCase(keyword);
}
というServiceがあるとします。
キーワードがある場合
例えば、
@Test
void キーワードがある場合は名前で検索する() {
// Arrange
Equipment equipment = new Equipment();
when(equipmentRepository
.findByNameContainingIgnoreCase("pump"))
.thenReturn(List.of(equipment));
// Act
List<Equipment> result =
equipmentService.search("pump");
// Assert
assertEquals(1, result.size());
verify(equipmentRepository)
.findByNameContainingIgnoreCase("pump");
}
このテストでは、
search("pump")
|
v
findByNameContainingIgnoreCase("pump")
|
v
Mockが設備を1件返す
|
v
Serviceが1件返す
ことを確認しています。
キーワードが空の場合
Serviceには、
if (keyword == null || keyword.isBlank()) {
return equipmentRepository.findAll();
}
という分岐があります。
そのため、このケースもテストできます。
@Test
void キーワードが空の場合は全件取得する() {
// Arrange
Equipment equipment = new Equipment();
when(equipmentRepository.findAll())
.thenReturn(List.of(equipment));
// Act
List<Equipment> result =
equipmentService.search("");
// Assert
assertEquals(1, result.size());
verify(equipmentRepository).findAll();
}
こうすると、
keywordあり
↓
検索
keywordなし
↓
全件取得
というServiceの分岐をそれぞれ確認できます。
さらに「呼ばれていない」ことも確認する
空文字の場合、
findAll()
を呼んでほしい一方で、
findByNameContainingIgnoreCase()
は呼んでほしくありません。
そこで、
verify(equipmentRepository)
.findAll();
verify(equipmentRepository, never())
.findByNameContainingIgnoreCase(anyString());
とすれば、
findAll()
↓
呼ばれた
検索メソッド
↓
呼ばれていない
ことまで確認できます。
Mockは「本物っぽいDB」ではない
ここは重要です。
Mock Repositoryは、
本物のRepositoryやDBの動作を再現しているわけではありません。
例えば、
when(repository.findAll())
.thenReturn(List.of(equipment));
と書いたから、その値を返しているだけです。
つまり、
SQLは正しい?
JPAのMappingは正しい?
PostgreSQLで動く?
検索クエリは正しい?
といったことは、このService単体テストでは確認できません。
それらはRepositoryテストや結合テストなど別のテストで確認します。
Service単体テストでは何を見る?
Serviceの単体テストで重要なのは、
Service自身のロジック
です。
例えば、
空文字ならfindAll()
キーワードがあれば検索
存在しなければ例外
登録前に重複チェック
条件によってRepositoryの呼び出しを変更
などです。
Repositoryが正しくSQLを発行するかをService単体テストで確認する必要はありません。
@SpringBootTestを使わなくてもいいの?
純粋なService単体テストなら、必ずしもSpring Boot全体を起動する必要はありません。
例えば、
@ExtendWith(MockitoExtension.class)
class EquipmentServiceTest {
@Mock
EquipmentRepository equipmentRepository;
@InjectMocks
EquipmentService equipmentService;
}
なら、
Spring Boot ApplicationContext
起動しない
↓
JUnit + Mockitoだけでテスト
という軽量なテストにできます。
Serviceのロジックだけ確認したいなら、この方が高速で対象も明確です。
まとめ
JUnit + Mockitoを使ったService単体テストでは、
JUnit
↓
テストを書く・実行する
Mockito
↓
依存オブジェクトをMockにする
という役割分担があります。
代表的なものを整理すると、
| 機能 | 役割 |
|---|---|
@Test |
テストメソッドを定義 |
@ExtendWith(MockitoExtension.class) |
JUnit 5でMockitoを利用 |
@Mock |
Mockを作成 |
@InjectMocks |
テスト対象へMockを注入 |
when() |
Mockの呼び出し条件を指定 |
thenReturn() |
Mockが返す値を指定 |
assertEquals() |
実行結果を確認 |
verify() |
Mockとのやり取りを確認 |
そしてService単体テストでは、
Arrange
↓
Mockの振る舞いを準備
Act
↓
本物のServiceを実行
Assert
↓
結果を確認
Verify
↓
依存先とのやり取りを確認
と考えると整理しやすくなります。
特に重要なのは、
RepositoryをMockにするのは、Repositoryをテストするためではなく、Serviceだけを独立してテストするため
という点です。
Repositoryそのものが正しくDBへアクセスできるかは、@DataJpaTestなどを使ったRepositoryテストで別途確認します。