【2024年保存版】Mockitoの使い方完全ガイド:実践的なユニットテスト11選

目次

目次へ

Mockitoとは:モックフレームワークの決定版

JavaでのユニットテストにおけるMockitoの役割と重要性

Mockitoは、Javaアプリケーションのユニットテストを効率的に行うための最も人気のあるモックフレームワークです。2024年現在、GitHub上で44,000以上のスターを獲得し、多くの企業での実績があります。

Mockitoが解決する主な課題
  1. 外部依存の分離
    • データベースアクセス
    • 外部APIとの通信
    • ファイルシステムの操作
    • 時間依存の処理
  2. テストの信頼性向上
    • 環境依存のないテスト実行
    • 再現性の高いテストケース
    • エッジケースのシミュレーション
  3. 開発効率の向上
    • テストコードの記述が簡潔
    • 直感的なAPI設計
    • 豊富なドキュメントとコミュニティサポート

他のモックフレームワークと比較したMockitoの優位性

主要なモックフレームワークの比較
特徴MockitoEasyMockPowerMockJMockit
学習曲線緩やか中程度
コード可読性
機能の豊富さ
メンテナンス
Spring対応
static/finalメソッド
コミュニティ活性度

Mockitoを選ぶべき理由

  1. 直感的なAPI設計
// Mockitoの場合
when(userService.findById(1L)).thenReturn(new User("John"));

// EasyMockの場合
expect(userService.findById(1L)).andReturn(new User("John"));
replay(userService);
  1. Spring Frameworkとの統合
    • Spring Boot Test内でのシームレスな統合
    • @MockBeanアノテーションによる簡単なモック注入
    • Spring DIコンテナとの相性の良さ
  2. 活発なコミュニティと継続的な改善
    • 定期的なバージョンアップデート
    • バグ修正の迅速な対応
    • 豊富なドキュメントとサンプルコード
  3. モダンな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() {
        // 各テストメソッド実行前の初期化
    }
}
よくある設定の問題と解決方法
問題原因解決方法
モックが機能しない@ExtendWithアノテーションの欠如クラスに@ExtendWith(MockitoExtension.class)を追加
バージョン不整合JUnitとMockitoのバージョンミスマッチ互換性のあるバージョンの組み合わせを使用
DIエラー@InjectMocksの対象クラスにコンストラクタ注入があるコンストラクタ用のモックを全て準備
Static/Finalメソッドのモック化失敗Mockito単体での制限PowerMockとの併用を検討

推奨される追加設定

  1. テストカバレッジツールの導入
<!-- 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>
  1. テストログの設定
<!-- 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);
}

重要な注意点

  1. マッチャーの混合使用
// 正しい使用法:全ての引数でマッチャーを使用
when(userService.findUser(any(), anyString())).thenReturn(new User());

// 誤った使用法:通常の引数とマッチャーの混合
// when(userService.findUser(1L, anyString())); // コンパイルエラー
  1. 検証時のマッチャー
@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() {
        // テストコード
    }
}
パフォーマンス改善のためのチェックリスト
改善項目実装方法期待効果
テストの独立性@DirtiesContextの適切な使用コンテキストの再利用による高速化
モックの再利用静的フィールドでのモック共有インスタンス生成コストの削減
非同期処理の最適化CompletableFutureReactiveStreamの活用待機時間の最小化
テストデータの最適化必要最小限のデータセット作成メモリ使用量の削減
主なトラブルと解決方法のまとめ
  1. NullPointerException対策
    • モックの適切な初期化確認
    • 戻り値の明示的な設定
    • Optional活用によるnull安全性の確保
  2. 検証失敗の対処
    • 引数マッチャーの適切な使用
    • 検証順序の明確化
    • デバッグ情報の出力強化
  3. パフォーマンス最適化
    • テストインスタンスのライフサイクル管理
    • 非同期処理の効率的なテスト
    • 適切なテストデータ設計

これらの問題に適切に対処することで、より信頼性が高く、保守性の良いテストコードを実現できます。特に、デバッグ情報の適切な活用とパフォーマンス最適化は、長期的なプロジェクトの成功に大きく貢献します。

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());
}
テスト可能性向上のためのチェックリスト
  1. 依存性の外部化
    • コンストラクタ注入の使用
    • インターフェースの活用
    • static メソッドの最小化
  2. 副作用の分離
    • ビジネスロジックとI/O処理の分離
    • トランザクション境界の明確化
    • 状態変更の追跡可能性
  3. テストフックの提供
    • protected メソッドの適切な活用
    • カスタマイズポイントの明確化
    • モック可能なコールバック

これらのテクニックを適切に組み合わせることで、テストの保守性と再利用性を大きく向上させることができます。特に、カスタムマッチャーとテストデータビルダーの活用は、テストコードの表現力と可読性を高める効果的な方法です。

実践的なユニットテスト事例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());
}

実践的なテストケースのまとめ

テスト設計のベストプラクティス

  1. コントローラーレイヤー
    • リクエスト/レスポンスの検証
    • バリデーションの確認
    • エラーハンドリングの網羅
    • セキュリティ関連のテスト
  2. サービスレイヤー
    • ビジネスロジックの検証
    • トランザクション処理の確認
    • 例外ハンドリングの確認
    • 依存サービスの適切なモック化
  3. リポジトリレイヤー
    • クエリ結果の検証
    • ページング/ソートの動作確認
    • データ整合性の検証
    • パフォーマンスの考慮

効果的なテストケース設計のポイント

観点検証ポイント実装方法
機能性基本機能の動作正常系テストの充実
堅牢性エラー処理異常系テストの網羅
性能応答時間/リソース負荷テストの実装
保守性コードの品質テストの可読性向上

これらのテストケースを適切に組み合わせることで、アプリケーションの品質を効果的に担保することができます。特に、各レイヤーの責務に応じたテストの設計と実装は、長期的なメンテナンス性の向上に大きく貢献します。

まとめ:効果的なユニットテスト実装に向けて

本記事のポイント整理

  1. Mockitoの基本と特徴
    • Java業界で最も使われているモックフレームワーク
    • 直感的なAPI設計による高い生産性
    • Spring Framework との優れた親和性
    • 豊富なコミュニティサポート
  2. 実装のベストプラクティス
    • モックオブジェクトの適切な初期化と管理
    • スタブの効果的な活用方法
    • 引数マッチャーによる柔軟なテスト実装
    • テストコードの再利用性向上テクニック
  3. 実践的なテストパターン
    • 各レイヤー(Controller/Service/Repository)に適したテスト設計
    • 外部依存の適切なモック化
    • 例外処理とエラーケースのテスト
    • 非同期処理のテスト手法

Mockitoを活用したテスト実装のチェックリスト

プロジェクトセットアップ

  • [ ] 適切なバージョンのMockito導入
  • [ ] JUnit 5との連携設定
  • [ ] Spring Bootプロジェクトでの設定確認

テストコード品質

  • [ ] テストの可読性確保
  • [ ] 適切な粒度でのテストケース分割
  • [ ] モックオブジェクトの明確な役割定義
  • [ ] テストデータの適切な準備

保守性向上

  • [ ] テストコードの再利用パターン活用
  • [ ] カスタムマッチャーの適切な実装
  • [ ] テスト可能な設計への改善
  • [ ] 効率的なテストスイートの構築

今後の学習ポイント

  1. 応用的なテスト手法
    • モックを活用したTDD(テスト駆動開発)の実践
    • マイクロサービスのテスト戦略
    • パフォーマンステストとの組み合わせ
  2. テスト自動化との統合
    • CI/CDパイプラインでの効果的な活用
    • テストカバレッジの継続的な監視
    • テスト結果の可視化と分析
  3. チーム開発での活用
    • テストコーディング規約の確立
    • コードレビューでのテスト品質チェック
    • ナレッジ共有と育成計画

最後に

Mockitoは単なるモックフレームワークではなく、品質の高いソフトウェア開発を支える重要なツールです。本記事で解説した内容を基に、プロジェクトの特性に合わせて適切にカスタマイズし、効果的なテスト戦略を構築していただければと思います。

テストコードの品質は、プロダクトコードの品質に直結します。Mockitoの機能を十分に理解し、適切に活用することで、保守性が高く、信頼性のあるアプリケーション開発を実現できるでしょう。