はじめに
Spring Bootでアプリを開発していると、「サービス層のロジックをテストしたいけど、DBへの接続はどうする?」という疑問に当たることがあります。
この記事では、そういった依存関係をモック化しながら、JUnit5 + Mockito を使ったサービス層の単体テストの書き方を解説します。
前提環境
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test には JUnit5 と Mockito が含まれているため、追加の依存関係は不要です。
テスト対象のコード
以下のようなユーザー管理サービスをテストします。
// User.java
@Entity
@Getter @Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("ユーザーが見つかりません: " + id));
}
public User save(User user) {
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
throw new IllegalArgumentException("このメールアドレスは既に使用されています");
}
return userRepository.save(user);
}
}
@Mock と @InjectMocks とは?
| アノテーション | 役割 |
|---|---|
@Mock |
モックオブジェクトを生成する。実際のDBアクセスは行わない |
@InjectMocks |
@Mock で生成したモックを注入したインスタンスを生成する |
つまり、UserService は UserRepository に依存していますが、テスト時は本物のDBを使わず、@Mock で作った偽の UserRepository を自動で注入してくれます。
テストクラスの基本構造
@ExtendWith(MockitoExtension.class) // JUnit5でMockitoを有効化
class UserServiceTest {
@Mock
private UserRepository userRepository; // モック(偽のRepository)
@InjectMocks
private UserService userService; // モックが注入されたService
// テストメソッドはここに書く
}
テスト① 正常系:ユーザーが見つかる場合
@Test
@DisplayName("IDに対応するユーザーが存在する場合、そのユーザーを返す")
void findById_success() {
// Arrange(準備)
Long userId = 1L;
User mockUser = new User();
mockUser.setId(userId);
mockUser.setName("田中 太郎");
// findById(1L) が呼ばれたら mockUser を返すよう設定
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
// Act(実行)
User result = userService.findById(userId);
// Assert(検証)
assertThat(result.getId()).isEqualTo(userId);
assertThat(result.getName()).isEqualTo("田中 太郎");
// findById が1回だけ呼ばれたことを確認
verify(userRepository, times(1)).findById(userId);
}
ポイント解説
-
when(...).thenReturn(...): モックの振る舞いを定義します。「このメソッドが呼ばれたらこれを返す」という設定です。 -
verify(...): モックのメソッドが期待通りに呼ばれたか検証します。
テスト② 異常系:ユーザーが見つからない場合
@Test
@DisplayName("IDに対応するユーザーが存在しない場合、RuntimeExceptionをスローする")
void findById_notFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> userService.findById(userId))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("ユーザーが見つかりません: 999");
}
テスト③ 正常系:ユーザー登録
@Test
@DisplayName("メールアドレスが未登録の場合、ユーザーを保存して返す")
void save_success() {
// Arrange
User newUser = new User();
newUser.setName("山田 花子");
newUser.setEmail("hanako@example.com");
when(userRepository.findByEmail("hanako@example.com")).thenReturn(Optional.empty());
when(userRepository.save(newUser)).thenReturn(newUser);
// Act
User result = userService.save(newUser);
// Assert
assertThat(result.getName()).isEqualTo("山田 花子");
verify(userRepository).save(newUser);
}
テスト④ 異常系:メールアドレス重複
@Test
@DisplayName("既存のメールアドレスで登録しようとした場合、IllegalArgumentExceptionをスローする")
void save_emailAlreadyExists() {
// Arrange
User existingUser = new User();
existingUser.setEmail("hanako@example.com");
User newUser = new User();
newUser.setEmail("hanako@example.com");
when(userRepository.findByEmail("hanako@example.com"))
.thenReturn(Optional.of(existingUser));
// Act & Assert
assertThatThrownBy(() -> userService.save(newUser))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("このメールアドレスは既に使用されています");
// saveは呼ばれないことを確認
verify(userRepository, never()).save(any());
}
よく使うMockitoのメソッド一覧
| メソッド | 用途 |
|---|---|
when(x).thenReturn(y) |
メソッドの戻り値を設定 |
when(x).thenThrow(e) |
例外をスローさせる |
verify(mock).method() |
メソッドが呼ばれたか確認 |
verify(mock, times(n)) |
n回呼ばれたか確認 |
verify(mock, never()) |
一度も呼ばれていないことを確認 |
any() / anyLong()
|
引数マッチャー(任意の値) |
ArgumentCaptor |
渡された引数の値をキャプチャして検証 |
まとめ
-
@Mockでリポジトリをモック化し、@InjectMocksでサービスに注入する -
when(...).thenReturn(...)でモックの振る舞いを定義する - 正常系・異常系の両方を書く習慣をつける
最初は「何をモックすべきか」に迷うかもしれませんが、テスト対象クラスの外側にある依存(DB、外部API等)はすべてモック化すると考えると整理しやすくなります。