27
3

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でMockを使用した単体テストの書き方

27
Posted at

はじめに

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 で生成したモックを注入したインスタンスを生成する

つまり、UserServiceUserRepository に依存していますが、テスト時は本物の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等)はすべてモック化すると考えると整理しやすくなります。

27
3
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
27
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?