Mockitoとは:モックフレームワークの決定版
JavaでのユニットテストにおけるMockitoの役割と重要性
Mockitoは、Javaアプリケーションのユニットテストを効率的に行うための最も人気のあるモックフレームワークです。2024年現在、GitHub上で44,000以上のスターを獲得し、多くの企業での実績があります。
他のモックフレームワークと比較したMockitoの優位性
特徴 | Mockito | EasyMock | PowerMock | JMockit |
---|---|---|---|---|
学習曲線 | 緩やか | 中程度 | 急 | 急 |
コード可読性 | ◎ | ○ | △ | ○ |
機能の豊富さ | ○ | ○ | ◎ | ◎ |
メンテナンス | ◎ | ○ | △ | △ |
Spring対応 | ◎ | ○ | ○ | ○ |
static/finalメソッド | △ | △ | ◎ | ◎ |
コミュニティ活性度 | ◎ | △ | ○ | △ |
Mockitoを選ぶべき理由
- 直感的なAPI設計
// Mockitoの場合 when(userService.findById(1L)).thenReturn(new User("John")); // EasyMockの場合 expect(userService.findById(1L)).andReturn(new User("John")); replay(userService);
- Spring Frameworkとの統合
- Spring Boot Test内でのシームレスな統合
@MockBean
アノテーションによる簡単なモック注入- Spring DIコンテナとの相性の良さ
- 活発なコミュニティと継続的な改善
- 定期的なバージョンアップデート
- バグ修正の迅速な対応
- 豊富なドキュメントとサンプルコード
- モダンなJava機能のサポート
- Java 8以降の機能との互換性
- ラムダ式のサポート
- Optional型の適切な処理
このように、Mockitoは学習コストが低く、実用的な機能を備え、かつモダンな開発環境に適応した理想的なモックフレームワークとして、多くの開発現場で採用されています。特にSpring Bootを使用するプロジェクトでは、テストフレームワークの選択肢として最も合理的な選択となるでしょう。
Mockitoをプロジェクトに導入する方法
Maven/Gradleでの依存関係の追加方法
Mavenの場合
pom.xml
に以下の依存関係を追加します:
<!-- JUnit 5 --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.1</version> <scope>test</scope> </dependency> <!-- Mockito Core --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.8.0</version> <scope>test</scope> </dependency> <!-- Mockito JUnit Jupiter --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>5.8.0</version> <scope>test</scope> </dependency>
Gradleの場合
build.gradle
に以下の依存関係を追加します:
dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' testImplementation 'org.mockito:mockito-core:5.8.0' testImplementation 'org.mockito:mockito-junit-jupiter:5.8.0' } test { useJUnitPlatform() }
JUnit 5との連携設定のベストプラクティス
1. テストクラスの基本設定
import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.Mock; import org.mockito.InjectMocks; @ExtendWith(MockitoExtension.class) // JUnit 5でMockitoを使用するための設定 public class UserServiceTest { @Mock // モックオブジェクトの作成 private UserRepository userRepository; @InjectMocks // モックを注入するクラス private UserService userService; // テストメソッド }
2. Spring Bootプロジェクトでの設定
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @SpringBootTest public class UserControllerTest { @MockBean // SpringのDIコンテナにモックを登録 private UserService userService; @Autowired private UserController userController; // テストメソッド }
3. テストライフサイクルの設定
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // クラス単位でインスタンスを作成 public class UserServiceTest { @BeforeAll void setup() { // テスト全体の初期化処理 } @BeforeEach void init() { // 各テストメソッド実行前の初期化 } }
推奨される追加設定
- テストカバレッジツールの導入
<!-- JaCoCo --> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin>
- テストログの設定
<!-- logback-test.xml --> <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE" /> </root> </configuration>
これらの設定を適切に行うことで、Mockitoを使用した効果的なユニットテスト環境を構築することができます。特にSpring Bootプロジェクトでは、これらの設定によってテストの信頼性と保守性を大きく向上させることができます。
Mockitoの基本機能と使い方
モックオブジェクトの作成方法(mock・spy)
1. @Mock
アノテーションを使用した方法
@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; // モックオブジェクトの作成 @Test void testFindUser() { // テストコード } }
2. Mockito.mock()メソッドを使用した方法
class UserServiceTest { private UserRepository userRepository = Mockito.mock(UserRepository.class); @Test void testFindUser() { // テストコード } }
3. spy()を使用した部分モック
class UserServiceTest { // 実際のオブジェクトの一部のみをモック化 private List<String> spyList = Mockito.spy(new ArrayList<>()); @Test void testListOperations() { // 一部のメソッドのみをモック化 doReturn(100).when(spyList).size(); // 実際のメソッドは通常通り動作 spyList.add("test"); assertEquals("test", spyList.get(0)); // 実際の動作 assertEquals(100, spyList.size()); // モック化された動作 } }
スタブの定義方法(when-thenReturn)
1. 基本的なスタブの定義
@Test void testUserFind() { // 戻り値の設定 User mockUser = new User("test-user"); when(userRepository.findById(1L)).thenReturn(mockUser); // 複数の戻り値を順番に返す when(userRepository.getStatus()) .thenReturn("active") .thenReturn("inactive"); // 例外をスローする when(userRepository.findById(999L)) .thenThrow(new UserNotFoundException("User not found")); }
2. void メソッドのスタブ
@Test void testVoidMethod() { // voidメソッドの場合はdoNothing()やdoThrow()を使用 doNothing().when(userRepository).delete(1L); doThrow(new RuntimeException()).when(userRepository).delete(999L); }
3. コールバックを使用したスタブ
@Test void testWithCallback() { when(userRepository.findById(anyLong())).thenAnswer(invocation -> { Long id = invocation.getArgument(0); return new User("user-" + id); }); }
引数マッチャーの使い方(any, eq)
1. 基本的なマッチャー
@Test void testArgumentMatchers() { // 任意の引数 when(userRepository.findById(any())).thenReturn(new User()); // 特定の型の任意の引数 when(userRepository.findByEmail(anyString())).thenReturn(new User()); // 特定の値との完全一致 when(userRepository.findById(eq(1L))).thenReturn(new User()); // null値の許容 when(userRepository.findByEmail(isNull())).thenReturn(null); }
2. 複合的なマッチャー
@Test void testComplexMatchers() { // 文字列パターン when(userRepository.findByEmail(matches(".*@example\\.com"))) .thenReturn(new User()); // 数値範囲 when(userRepository.findByAge(intThat(age -> age >= 18 && age <= 60))) .thenReturn(Arrays.asList(new User(), new User())); }
3. カスタムマッチャー
class IsValidUserMatcher implements ArgumentMatcher<User> { @Override public boolean matches(User user) { return user != null && user.getEmail() != null && user.getEmail().contains("@"); } } @Test void testCustomMatcher() { // カスタムマッチャーの使用 when(userRepository.save(argThat(new IsValidUserMatcher()))) .thenReturn(true); }
重要な注意点
- マッチャーの混合使用
// 正しい使用法:全ての引数でマッチャーを使用 when(userService.findUser(any(), anyString())).thenReturn(new User()); // 誤った使用法:通常の引数とマッチャーの混合 // when(userService.findUser(1L, anyString())); // コンパイルエラー
- 検証時のマッチャー
@Test void testVerification() { userService.createUser("test@example.com", "password"); // マッチャーを使用した検証 verify(userRepository).save( argThat(user -> user.getEmail().equals("test@example.com") && user.getPassword() != null ) ); }
これらの基本機能を適切に組み合わせることで、柔軟で保守性の高いテストコードを作成することができます。特に、引数マッチャーを効果的に活用することで、テストコードの冗長性を減らし、より表現力豊かなテストを実現できます。
実践的なユニットテストパターン5選
外部APIとの通信をモックする方法
1. RestTemplateのモック化
@ExtendWith(MockitoExtension.class) class ExternalApiServiceTest { @Mock private RestTemplate restTemplate; @InjectMocks private ExternalApiService apiService; @Test void testExternalApiCall() { // レスポンスの準備 ResponseEntity<UserDTO> response = new ResponseEntity<>( new UserDTO("John Doe", "john@example.com"), HttpStatus.OK ); // モックの設定 when(restTemplate.exchange( anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDTO.class) )).thenReturn(response); // テスト実行 UserDTO result = apiService.fetchUserData(1L); // 検証 assertNotNull(result); assertEquals("John Doe", result.getName()); verify(restTemplate).exchange( contains("/api/users/1"), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDTO.class) ); } }
2. WebClient(非同期API)のモック化
@ExtendWith(MockitoExtension.class) class WebClientServiceTest { @Mock private WebClient.Builder webClientBuilder; @Mock private WebClient webClient; @Mock private WebClient.RequestHeadersUriSpec<?> requestHeadersUriSpec; @Mock private WebClient.ResponseSpec responseSpec; @InjectMocks private WebClientService webClientService; @Test void testAsyncApiCall() { // モックチェーンの設定 when(webClientBuilder.build()).thenReturn(webClient); when(webClient.get()).thenReturn(requestHeadersUriSpec); when(requestHeadersUriSpec.uri(anyString())).thenReturn(requestHeadersUriSpec); when(requestHeadersUriSpec.retrieve()).thenReturn(responseSpec); when(responseSpec.bodyToMono(UserDTO.class)) .thenReturn(Mono.just(new UserDTO("Jane Doe", "jane@example.com"))); // テスト実行 UserDTO result = webClientService.fetchUserData(1L).block(); // 検証 assertNotNull(result); assertEquals("Jane Doe", result.getName()); } }
データベース操作のテスト手法
1. リポジトリレイヤーのモック化
@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void testDatabaseOperation() { // テストデータの準備 User user = new User("test-user", "test@example.com"); when(userRepository.findById(1L)).thenReturn(Optional.of(user)); when(userRepository.save(any(User.class))).thenAnswer(i -> i.getArgument(0)); // テスト実行 User updatedUser = userService.updateUserEmail(1L, "new@example.com"); // 検証 assertEquals("new@example.com", updatedUser.getEmail()); verify(userRepository).save(argThat(u -> u.getEmail().equals("new@example.com") && u.getName().equals("test-user") )); } }
2. トランザクション処理のテスト
@ExtendWith(MockitoExtension.class) class TransactionServiceTest { @Mock private UserRepository userRepository; @Mock private AccountRepository accountRepository; @InjectMocks private TransactionService transactionService; @Test void testTransactionRollback() { // 失敗するトランザクションのシミュレーション when(userRepository.findById(1L)) .thenReturn(Optional.of(new User("test-user", 1000.0))); when(accountRepository.save(any())) .thenThrow(new RuntimeException("Database error")); // テスト実行と検証 assertThrows(TransactionException.class, () -> transactionService.transfer(1L, 2L, 500.0)); // ロールバックの確認 verify(userRepository, never()).save(any()); } }
例外処理のテストテクニック
1. 予期された例外のテスト
@Test void testExpectedExceptionHandling() { // 例外をスローするモックの設定 when(userRepository.findById(999L)) .thenThrow(new UserNotFoundException("User not found")); // 例外の検証 UserNotFoundException exception = assertThrows( UserNotFoundException.class, () -> userService.getUser(999L) ); assertEquals("User not found", exception.getMessage()); }
2. 例外チェーンのテスト
@Test void testExceptionChainHandling() { // 低レベルの例外をスローするモックの設定 when(userRepository.save(any())) .thenThrow(new DataIntegrityViolationException("Duplicate email")); // 高レベルの例外に変換されることを検証 BusinessException exception = assertThrows( BusinessException.class, () -> userService.createUser(new UserDTO("test", "existing@example.com")) ); assertTrue(exception.getCause() instanceof DataIntegrityViolationException); }
非同期処理のテスト方法
1. CompletableFutureのテスト
@Test void testAsyncOperation() { // 非同期処理のモック設定 when(userRepository.findByIdAsync(1L)) .thenReturn(CompletableFuture.completedFuture(new User("test-user"))); // テスト実行 CompletableFuture<User> future = userService.getUserAsync(1L); // 結果の検証 User result = future.join(); assertNotNull(result); assertEquals("test-user", result.getName()); }
2. Reactor/WebFluxのテスト
@Test void testReactiveStream() { // リアクティブストリームのモック設定 when(userRepository.findAllReactive()) .thenReturn(Flux.just( new User("user1"), new User("user2") )); // テスト実行と検証 StepVerifier.create(userService.getAllUsers()) .expectNextMatches(user -> user.getName().equals("user1")) .expectNextMatches(user -> user.getName().equals("user2")) .verifyComplete(); }
Springコンポーネントのテスト戦略
1. サービスレイヤーのテスト
@ExtendWith(MockitoExtension.class) class UserServiceIntegrationTest { @Mock private UserRepository userRepository; @Mock private EmailService emailService; @InjectMocks private UserService userService; @Test void testUserRegistration() { // モックの設定 when(userRepository.save(any(User.class))) .thenAnswer(i -> i.getArgument(0)); doNothing().when(emailService) .sendWelcomeEmail(anyString()); // テスト実行 User newUser = userService.registerUser( new UserDTO("test-user", "test@example.com") ); // 検証 assertNotNull(newUser); verify(emailService).sendWelcomeEmail("test@example.com"); verify(userRepository).save(any(User.class)); } }
2. コントローラーレイヤーのテスト
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testGetUser() throws Exception { // モックの設定 when(userService.getUser(1L)) .thenReturn(new User("test-user", "test@example.com")); // APIリクエストのテスト mockMvc.perform(get("/api/users/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("test-user")) .andExpect(jsonPath("$.email").value("test@example.com")); } }
これらのパターンを適切に組み合わせることで、信頼性の高いテストスイートを構築することができます。特に、外部依存のモック化と例外処理のテストは、アプリケーションの堅牢性を確保する上で重要な役割を果たします。
よくあるトラブルと解決方法
NullPointerExceptionの原因と対処法
1. モックの初期化失敗
// 問題のあるコード public class UserServiceTest { private UserRepository userRepository; // @Mockアノテーション忘れ @Test void testUser() { when(userRepository.findById(1L)) // NullPointerException発生 .thenReturn(new User()); } } // 正しい実装 @ExtendWith(MockitoExtension.class) public class UserServiceTest { @Mock private UserRepository userRepository; // 適切な初期化 @Test void testUser() { when(userRepository.findById(1L)) // 正常に動作 .thenReturn(new User()); } }
2. モック化されたメソッドの戻り値がnull
@Test void testUserSearch() { // 暗黙的にnullを返すモック UserRepository userRepository = mock(UserRepository.class); // 明示的に戻り値を設定 when(userRepository.findById(anyLong())) .thenReturn(Optional.of(new User())); // NullPointerを防ぐ // Optional使用のベストプラクティス when(userRepository.findById(anyLong())) .thenReturn(Optional.empty()); // nullの代わりにEmpty Optionalを使用 }
3. 部分モック(spy)使用時の注意点
// 問題のあるコード List<String> spyList = spy(null); // NullPointerException // 正しい実装 List<String> spyList = spy(new ArrayList<>()); // 実オブジェクトの指定が必要
モックの検証失敗時のデバッグ方法
1. 引数マッチャーの不一致
@Test void testArgumentMatching() { UserService userService = mock(UserService.class); // 問題のあるコード when(userService.findUser(1L, "test")) // 具体的な値を使用 .thenReturn(new User()); // 正しい実装 when(userService.findUser(eq(1L), anyString())) // マッチャーを使用 .thenReturn(new User()); // 検証時のデバッグ verify(userService, times(1)).findUser( argThat(arg -> { System.out.println("Actual argument: " + arg); // 実際の引数を出力 return arg.equals(1L); }), anyString() ); }
2. 検証順序の問題
@Test void testVerificationOrder() { UserService userService = mock(UserService.class); // 実行 userService.createUser(new User()); userService.updateUser(new User()); // 問題のある検証(順序が重要な場合) InOrder inOrder = inOrder(userService); inOrder.verify(userService).updateUser(any()); // 順序違反 inOrder.verify(userService).createUser(any()); // デバッグのための検証 Mockito.verifyNoMoreInteractions(userService); // 未検証の呼び出しを確認 }
テストの実行速度改善テクニック
1. テストスイートの最適化
// 遅いテスト実装 @Test void slowTest() throws InterruptedException { Thread.sleep(1000); // 不要な待機時間 verify(userService).process(); } // 最適化されたテスト @Test void optimizedTest() { // 非同期処理のテスト改善 CompletableFuture<User> future = userService.processAsync(); User result = future.join(); // 必要な待機のみ verify(userService).processAsync(); }
2. パフォーマンス改善のベストプラクティス
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // インスタンス生成の最適化 public class OptimizedTest { private static UserService userService; // 共有インスタンス @BeforeAll void setup() { userService = mock(UserService.class); // 一度だけ初期化 } @Test void test1() { // テストコード } @Test void test2() { // テストコード } }
- NullPointerException対策
- モックの適切な初期化確認
- 戻り値の明示的な設定
- Optional活用によるnull安全性の確保
- 検証失敗の対処
- 引数マッチャーの適切な使用
- 検証順序の明確化
- デバッグ情報の出力強化
- パフォーマンス最適化
- テストインスタンスのライフサイクル管理
- 非同期処理の効率的なテスト
- 適切なテストデータ設計
これらの問題に適切に対処することで、より信頼性が高く、保守性の良いテストコードを実現できます。特に、デバッグ情報の適切な活用とパフォーマンス最適化は、長期的なプロジェクトの成功に大きく貢献します。
Mockitoを使った開発効率化テクニック
テストコードの再利用パターン
1. テストフィクスチャの共有
@ExtendWith(MockitoExtension.class) public abstract class BaseUserTest { @Mock protected UserRepository userRepository; @Mock protected EmailService emailService; // 共通のモックセットアップ protected void setupCommonMocks() { when(userRepository.findById(anyLong())) .thenReturn(Optional.of(new User("test-user"))); when(emailService.sendEmail(anyString(), anyString())) .thenReturn(true); } // 共通の検証ロジック protected void verifyEmailNotification(String email) { verify(emailService).sendEmail( eq(email), contains("notification") ); } } // 具体的なテストクラス public class UserServiceTest extends BaseUserTest { @InjectMocks private UserService userService; @Test void testUserUpdate() { setupCommonMocks(); // 共通セットアップの利用 userService.updateUser(1L, "new@example.com"); verifyEmailNotification("new@example.com"); // 共通検証の利用 } }
2. テストデータビルダー
public class UserTestBuilder { private Long id = 1L; private String name = "default-name"; private String email = "default@example.com"; private UserStatus status = UserStatus.ACTIVE; public static UserTestBuilder aUser() { return new UserTestBuilder(); } public UserTestBuilder withId(Long id) { this.id = id; return this; } public UserTestBuilder withName(String name) { this.name = name; return this; } public UserTestBuilder withEmail(String email) { this.email = email; return this; } public User build() { User user = new User(); user.setId(id); user.setName(name); user.setEmail(email); user.setStatus(status); return user; } } // テストでの使用例 @Test void testUserCreation() { User testUser = UserTestBuilder.aUser() .withName("Test User") .withEmail("test@example.com") .build(); when(userRepository.save(any(User.class))) .thenReturn(testUser); }
カスタムマッチャーの作成方法
1. 基本的なカスタムマッチャー
public class UserMatcher implements ArgumentMatcher<User> { private final String expectedEmail; public UserMatcher(String expectedEmail) { this.expectedEmail = expectedEmail; } @Override public boolean matches(User user) { return user != null && expectedEmail.equals(user.getEmail()); } @Override public String toString() { return "User with email: " + expectedEmail; } } // 使用例 @Test void testUserMatching() { User user = new User("test@example.com"); ArgumentMatcher<User> matcher = new UserMatcher("test@example.com"); when(userRepository.save(argThat(matcher))) .thenReturn(user); userService.createUser(user); verify(userRepository).save(argThat(matcher)); }
2. 汎用的なマッチャーファクトリー
public class CustomMatchers { public static ArgumentMatcher<User> hasEmail(String email) { return new UserMatcher(email); } public static ArgumentMatcher<User> hasValidAge() { return user -> user != null && user.getAge() >= 0 && user.getAge() <= 120; } public static <T> ArgumentMatcher<List<T>> hasSize(int size) { return list -> list != null && list.size() == size; } } // 使用例 @Test void testCustomMatchers() { when(userRepository.findByEmail(argThat(hasEmail("test@example.com")))) .thenReturn(Optional.of(new User())); verify(userRepository).saveAll(argThat(hasSize(2))); }
テスト可能な設計への改善方法
1. 依存性の明確化と注入
// 改善前のコード public class UserService { private final UserRepository userRepository = new UserRepository(); // 直接インスタンス化 public User createUser(String name) { return userRepository.save(new User(name)); } } // 改善後のコード public class UserService { private final UserRepository userRepository; private final EmailService emailService; @Autowired // コンストラクタ注入 public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; this.emailService = emailService; } public User createUser(String name) { User user = userRepository.save(new User(name)); emailService.sendWelcomeEmail(user.getEmail()); return user; } }
2. インターフェースの活用
// インターフェース定義 public interface EmailService { void sendWelcomeEmail(String email); void sendNotification(String email, String message); } // 実装クラス @Service public class EmailServiceImpl implements EmailService { @Override public void sendWelcomeEmail(String email) { // 実装 } @Override public void sendNotification(String email, String message) { // 実装 } } // テストでの活用 @Test void testEmailNotification() { EmailService mockEmailService = mock(EmailService.class); UserService userService = new UserService(userRepository, mockEmailService); userService.createUser("test-user"); verify(mockEmailService).sendWelcomeEmail(anyString()); }
これらのテクニックを適切に組み合わせることで、テストの保守性と再利用性を大きく向上させることができます。特に、カスタムマッチャーとテストデータビルダーの活用は、テストコードの表現力と可読性を高める効果的な方法です。
実践的なユニットテスト事例6選
RESTコントローラーのテストコード例
1. ユーザー登録APIのテスト
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testCreateUser() throws Exception { // テストデータ準備 UserDTO userDTO = new UserDTO("test-user", "test@example.com"); User createdUser = new User(1L, "test-user", "test@example.com"); when(userService.createUser(any(UserDTO.class))) .thenReturn(createdUser); // APIリクエストの実行と検証 mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(userDTO))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("test-user")) .andExpect(jsonPath("$.email").value("test@example.com")); } }
2. エラーハンドリングのテスト
@Test void testUserNotFound() throws Exception { when(userService.getUser(999L)) .thenThrow(new UserNotFoundException("User not found")); mockMvc.perform(get("/api/users/999") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").value("User not found")) .andExpect(jsonPath("$.timestamp").exists()); }
サービスレイヤーのテストパターン
1. ユーザー認証サービスのテスト
@ExtendWith(MockitoExtension.class) class AuthenticationServiceTest { @Mock private UserRepository userRepository; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private AuthenticationService authService; @Test void testSuccessfulAuthentication() { // テストデータ準備 String email = "test@example.com"; String password = "password123"; User user = new User(email, "hashedPassword"); when(userRepository.findByEmail(email)) .thenReturn(Optional.of(user)); when(passwordEncoder.matches(password, user.getPassword())) .thenReturn(true); // 認証実行と検証 AuthenticationResult result = authService.authenticate(email, password); assertTrue(result.isSuccess()); assertEquals(user, result.getUser()); } @Test void testFailedAuthentication() { when(userRepository.findByEmail(anyString())) .thenReturn(Optional.empty()); AuthenticationResult result = authService.authenticate( "wrong@example.com", "wrongpass"); assertFalse(result.isSuccess()); assertNull(result.getUser()); } }
2. トランザクション処理のテスト
@ExtendWith(MockitoExtension.class) class TransferServiceTest { @Mock private AccountRepository accountRepository; @Mock private TransactionRepository transactionRepository; @InjectMocks private TransferService transferService; @Test void testSuccessfulTransfer() { // テストデータ準備 Account sourceAccount = new Account(1L, "source", 1000.0); Account targetAccount = new Account(2L, "target", 500.0); when(accountRepository.findById(1L)) .thenReturn(Optional.of(sourceAccount)); when(accountRepository.findById(2L)) .thenReturn(Optional.of(targetAccount)); // 送金実行と検証 TransferResult result = transferService.transfer(1L, 2L, 300.0); assertTrue(result.isSuccess()); assertEquals(700.0, sourceAccount.getBalance()); assertEquals(800.0, targetAccount.getBalance()); verify(accountRepository, times(2)).save(any(Account.class)); verify(transactionRepository).save(any(Transaction.class)); } }
リポジトリレイヤーのモック活用例
1. 複合条件での検索テスト
@ExtendWith(MockitoExtension.class) class UserRepositoryTest { @Mock private JpaRepository<User, Long> jpaRepository; @InjectMocks private CustomUserRepository userRepository; @Test void testFindActiveUsersByRole() { // テストデータ準備 List<User> activeAdmins = Arrays.asList( new User("admin1", Role.ADMIN), new User("admin2", Role.ADMIN) ); when(jpaRepository.findAll(any(Specification.class))) .thenReturn(activeAdmins); // 検索実行と検証 List<User> result = userRepository.findActiveUsersByRole(Role.ADMIN); assertEquals(2, result.size()); assertTrue(result.stream() .allMatch(user -> user.getRole() == Role.ADMIN)); } }
2. ページング処理のテスト
@Test void testFindUsersWithPagination() { // テストデータ準備 Page<User> userPage = new PageImpl<>( Arrays.asList(new User("user1"), new User("user2")), PageRequest.of(0, 10), 2 ); when(jpaRepository.findAll(any(Pageable.class))) .thenReturn(userPage); // ページング検索実行と検証 Page<User> result = userRepository.findUsers(PageRequest.of(0, 10)); assertEquals(2, result.getContent().size()); assertEquals(1, result.getTotalPages()); assertEquals(2, result.getTotalElements()); }
実践的なテストケースのまとめ
テスト設計のベストプラクティス
- コントローラーレイヤー
- リクエスト/レスポンスの検証
- バリデーションの確認
- エラーハンドリングの網羅
- セキュリティ関連のテスト
- サービスレイヤー
- ビジネスロジックの検証
- トランザクション処理の確認
- 例外ハンドリングの確認
- 依存サービスの適切なモック化
- リポジトリレイヤー
- クエリ結果の検証
- ページング/ソートの動作確認
- データ整合性の検証
- パフォーマンスの考慮
効果的なテストケース設計のポイント
観点 | 検証ポイント | 実装方法 |
---|---|---|
機能性 | 基本機能の動作 | 正常系テストの充実 |
堅牢性 | エラー処理 | 異常系テストの網羅 |
性能 | 応答時間/リソース | 負荷テストの実装 |
保守性 | コードの品質 | テストの可読性向上 |
これらのテストケースを適切に組み合わせることで、アプリケーションの品質を効果的に担保することができます。特に、各レイヤーの責務に応じたテストの設計と実装は、長期的なメンテナンス性の向上に大きく貢献します。
まとめ:効果的なユニットテスト実装に向けて
本記事のポイント整理
- Mockitoの基本と特徴
- Java業界で最も使われているモックフレームワーク
- 直感的なAPI設計による高い生産性
- Spring Framework との優れた親和性
- 豊富なコミュニティサポート
- 実装のベストプラクティス
- モックオブジェクトの適切な初期化と管理
- スタブの効果的な活用方法
- 引数マッチャーによる柔軟なテスト実装
- テストコードの再利用性向上テクニック
- 実践的なテストパターン
- 各レイヤー(Controller/Service/Repository)に適したテスト設計
- 外部依存の適切なモック化
- 例外処理とエラーケースのテスト
- 非同期処理のテスト手法
Mockitoを活用したテスト実装のチェックリスト
✅ プロジェクトセットアップ
- [ ] 適切なバージョンのMockito導入
- [ ] JUnit 5との連携設定
- [ ] Spring Bootプロジェクトでの設定確認
✅ テストコード品質
- [ ] テストの可読性確保
- [ ] 適切な粒度でのテストケース分割
- [ ] モックオブジェクトの明確な役割定義
- [ ] テストデータの適切な準備
✅ 保守性向上
- [ ] テストコードの再利用パターン活用
- [ ] カスタムマッチャーの適切な実装
- [ ] テスト可能な設計への改善
- [ ] 効率的なテストスイートの構築
今後の学習ポイント
- 応用的なテスト手法
- モックを活用したTDD(テスト駆動開発)の実践
- マイクロサービスのテスト戦略
- パフォーマンステストとの組み合わせ
- テスト自動化との統合
- CI/CDパイプラインでの効果的な活用
- テストカバレッジの継続的な監視
- テスト結果の可視化と分析
- チーム開発での活用
- テストコーディング規約の確立
- コードレビューでのテスト品質チェック
- ナレッジ共有と育成計画
最後に
Mockitoは単なるモックフレームワークではなく、品質の高いソフトウェア開発を支える重要なツールです。本記事で解説した内容を基に、プロジェクトの特性に合わせて適切にカスタマイズし、効果的なテスト戦略を構築していただければと思います。
テストコードの品質は、プロダクトコードの品質に直結します。Mockitoの機能を十分に理解し、適切に活用することで、保守性が高く、信頼性のあるアプリケーション開発を実現できるでしょう。