0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JUnit + MockitoでSpring BootのService単体テストを書く

0
Posted at

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テストで別途確認します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?