PowerMockとMockitoを用いた高度なJava単体テスト手法

本記事では、JUnit 4と組み合わせてPowerMockおよびMockitoを使用し、通常では困難なシナリオに対応するJava単体テストを記述する方法について解説します。静的メソッド、プライベートメソッド、コンストラクタ、あるいはfinalクラスやメソッドのモック化といった、より高度なテストケースへのアプローチを具体的なコード例を交えて紹介します。

1. 必須依存関係の設定

まず、pom.xmlに以下のMaven依存関係が定義されていることを確認してください。これにより、JUnit 4、PowerMockのJUnitランナーモジュール、そしてMockitoとの統合APIが利用可能になります。

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>2.0.9</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>2.0.9</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2. テストクラスの基本構造

PowerMockを利用するテストクラスは、@RunWith(PowerMockRunner.class)アノテーションでPowerMockのカスタムランナーを指定し、@PrepareForTestアノテーションでモック化する対象のクラスを宣言する必要があります。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class) // PowerMockテストランナーを使用
@PrepareForTest(TargetClassWithSpecialMethods.class) // 静的メソッドなどを持つクラスを指定
public class SystemComponentTest {

    @Test
    public void testComponentFunctionality() {
        // テストロジックをここに記述
    }
}

3. 主要なモック化シナリオとコード例

3.1. 静的メソッドのモック化

静的メソッドは通常のMockitoではモックできませんが、PowerMockを使用すれば可能です。PowerMockito.mockStatic()でクラス全体をモック化し、PowerMockito.when()で振る舞いを定義します。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

// 被テストクラス(静的メソッドを持つ)
class ConfigurationProvider {
    public static String getSystemSetting(String key) {
        return "Actual value for " + key; // 実際の処理
    }
}

// テストクラス
@RunWith(PowerMockRunner.class)
@PrepareForTest(ConfigurationProvider.class) // 静的メソッドを持つクラスを指定
public class StaticMethodMockingTest {

    @Test
    public void testStaticConfigurationRetrieval() {
        PowerMockito.mockStatic(ConfigurationProvider.class); // ConfigurationProviderクラスの静的メソッドをモック化
        String expectedSetting = "Mocked system setting";
        PowerMockito.when(ConfigurationProvider.getSystemSetting("app.name")).thenReturn(expectedSetting);

        // テスト対象のメソッドを実行
        String retrievedSetting = ConfigurationProvider.getSystemSetting("app.name");

        // 結果を検証
        Assert.assertEquals(expectedSetting, retrievedSetting);
    }
}

3.2. プライベートメソッドのモック化

プライベートメソッドもPowerMockでモック化できます。対象のインスタンスをPowerMockito.spy()でスパイオブジェクトに変換し、PowerMockito.doReturn().when()を使ってプライベートメソッドの振る舞いを上書きします。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

// 被テストクラス(プライベートメソッドを持つ)
class ReportService {
    private String processDataInternal(String rawData) {
        return "Processed:" + rawData; // 実際のプライベート処理
    }

    public String generateReport(String inputData) {
        return processDataInternal(inputData); // プライベートメソッドを呼び出すパブリックメソッド
    }
}

// テストクラス
@RunWith(PowerMockRunner.class)
@PrepareForTest(ReportService.class) // プライベートメソッドを持つクラスを指定
public class PrivateMethodMockingTest {

    @Test
    public void testPrivateMethodCallInPublic() throws Exception {
        ReportService service = PowerMockito.spy(new ReportService()); // スパイオブジェクトを作成
        String expectedOutput = "Mocked internal processing";

        // プライベートメソッド 'processDataInternal' の振る舞いをモック化
        PowerMockito.doReturn(expectedOutput).when(service, "processDataInternal", ArgumentMatchers.anyString());

        // パブリックメソッドを実行
        String result = service.generateReport("input_value");

        // 結果を検証
        Assert.assertEquals(expectedOutput, result);
    }
}

3.3. コンストラクタのモック化

クラス内でnewキーワードによって生成される依存オブジェクトをモック化したい場合、PowerMockのコンストラクタモックが役立ちます。PowerMockito.whenNew().withAnyArguments().thenReturn()を使用します。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.mockito.Mockito.mock;

// 内部で生成される依存クラス
class PaymentService {
    public String processPayment(double amount) {
        return "Real payment for " + amount + " processed."; // 実際の決済処理
    }
}

// 被テストクラス(コンストラクタ内で依存を生成)
class OrderHandler {
    private PaymentService paymentService;

    public OrderHandler() {
        this.paymentService = new PaymentService(); // コンストラクタ内でPaymentServiceを生成
    }

    public String handleOrder(double orderAmount) {
        // ... some order processing logic
        return paymentService.processPayment(orderAmount);
    }
}

// テストクラス
@RunWith(PowerMockRunner.class)
@PrepareForTest(OrderHandler.class) // コンストラクタ呼び出しを行うクラスを指定
public class ConstructorMockingTest {

    @Test
    public void testOrderProcessingWithMockedPaymentService() throws Exception {
        PaymentService mockPaymentService = mock(PaymentService.class);
        String expectedPaymentResult = "Mocked payment successful";

        // PaymentServiceのコンストラクタ呼び出しをモック化し、指定したモックインスタンスを返すように設定
        PowerMockito.whenNew(PaymentService.class).withNoArguments().thenReturn(mockPaymentService);
        // モックされたPaymentServiceインスタンスの振る舞いを定義
        PowerMockito.when(mockPaymentService.processPayment(ArgumentMatchers.anyDouble())).thenReturn(expectedPaymentResult);

        OrderHandler handler = new OrderHandler(); // このnew OrderHandler()の際に、PaymentServiceのモックが注入される
        String actualResult = handler.handleOrder(100.0);

        Assert.assertEquals(expectedPaymentResult, actualResult);
    }
}

3.4. Mockitoとの連携

PowerMockはMockitoの拡張として機能するため、両者を組み合わせて通常のオブジェクトと特殊なモック化(静的、プライベートなど)を同時に行うことができます。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

// 通常のサービスインターフェース
interface IDataProcessor {
    String processData();
}

// 静的メソッドを持つユーティリティクラス
class SystemLogger {
    public static void logMessage(String message) {
        System.out.println("LOG: " + message); // 実際のロギング処理
    }
    public static String getLogLevel() {
        return "INFO"; // 実際のログレベル
    }
}

// テストクラス
@RunWith(PowerMockRunner.class)
@PrepareForTest(SystemLogger.class) // 静的メソッドを持つSystemLoggerクラスをPowerMockで準備
public class CombinedMockingTest {

    @Test
    public void testDataProcessingAndLogging() {
        // 通常のオブジェクトをMockitoでモック化
        IDataProcessor mockProcessor = mock(IDataProcessor.class);
        when(mockProcessor.processData()).thenReturn("Mocked processed data");

        // 静的メソッドを持つクラスをPowerMockでモック化
        PowerMockito.mockStatic(SystemLogger.class);
        PowerMockito.when(SystemLogger.getLogLevel()).thenReturn("DEBUG");
        // voidメソッドのモックも同様に行えます
        PowerMockito.doNothing().when(SystemLogger.class);
        SystemLogger.logMessage(ArgumentMatchers.anyString()); // anyString()で任意の文字列を受け付ける

        String processorResult = mockProcessor.processData();
        String logLevel = SystemLogger.getLogLevel();

        String combinedResult = processorResult + " - " + logLevel;
        Assert.assertEquals("Mocked processed data - DEBUG", combinedResult);

        // 静的voidメソッドの呼び出しも検証可能
        PowerMockito.verifyStatic(SystemLogger.class);
        SystemLogger.logMessage(ArgumentMatchers.anyString()); // 実際に呼び出されたことを検証
    }
}

4. 振る舞いの検証

PowerMockを使ってモック化した静的メソッドの呼び出し回数や引数を検証するには、PowerMockito.verifyStatic()を使用します。これはMockitoのverify()と似た働きをしますが、静的メソッド専用です。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.mockito.Mockito.times;

// 静的メソッドを持つサービス
class NotificationService {
    public static void sendAlert(String message) {
        System.out.println("Alert sent: " + message); // 実際の通知処理
    }
}

// テストクラス
@RunWith(PowerMockRunner.class)
@PrepareForTest(NotificationService.class)
public class StaticMethodVerificationTest {

    @Test
    public void testAlertIsSentOnce() {
        PowerMockito.mockStatic(NotificationService.class); // NotificationServiceをモック化

        // 静的メソッドを呼び出す
        NotificationService.sendAlert("Critical issue detected");
        NotificationService.sendAlert("Critical issue detected"); // 2回呼び出し

        // sendAlertが2回呼び出されたことを検証
        PowerMockito.verifyStatic(NotificationService.class, times(2));
        NotificationService.sendAlert("Critical issue detected"); // 検証対象のメソッドを再度呼び出す
    }
}

5. 重要な考慮事項

  • @PrepareForTestの役割: 静的メソッド、プライベートメソッド、コンストラクタ、またはfinalクラス/メソッドをモック化する際は、必ずテストクラスに@PrepareForTest(ClassToMock.class)アノテーションを追加する必要があります。複数のクラスを対象にする場合は、@PrepareForTest({Class1.class, Class2.class})のように指定します。
  • Mockitoとの使い分け: 通常のオブジェクトやインターフェースのモックにはMockitoのmock()を優先して使用し、PowerMockは静的・プライベートメソッドやコンストラクタのモックなど、Mockitoだけでは対応できない特殊なケースに限定して使用することが推奨されます。
  • 過度な使用の回避: PowerMockはJavaのクラスローダーを操作するため、テストの実行速度に影響を与える可能性があります。また、テストコードが複雑になりがちです。可能な限りシンプルなテストを心がけ、PowerMockは本当に必要な場合にのみ利用しましょう。

6. 応用テクニック

6.1. finalクラス / finalメソッドのモック化

PowerMockは、デフォルトではモック化できないfinalクラスやfinalメソッドもモック化する能力を持っています。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

// finalクラス
final class ImmutableConfig {
    public final String getVersion() { // finalメソッド
        return "v1.0.0-production"; // 実際のバージョン情報
    }
}

// テストクラス
@RunWith(PowerMockRunner.class)
@PrepareForTest(ImmutableConfig.class) // finalクラスを指定
public class FinalClassMethodMockingTest {

    @Test
    public void testMockingFinalMethod() {
        ImmutableConfig mockConfig = PowerMockito.mock(ImmutableConfig.class); // finalクラスをモック化
        String expectedVersion = "mocked-v2.1.0-dev";
        PowerMockito.when(mockConfig.getVersion()).thenReturn(expectedVersion); // finalメソッドの振る舞いを定義

        String actualVersion = mockConfig.getVersion();
        Assert.assertEquals(expectedVersion, actualVersion);
    }
}

6.2. 部分的なモック(Spy)

オブジェクトの特定メソッドのみをモック化し、他のメソッドは実際の処理を維持したい場合に「部分的なモック(Partial Mocking)」または「スパイ(Spy)」を使用します。PowerMockito.spy()で対象インスタンスのスパイを作成し、モック化したいメソッドの振る舞いを定義します。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

// 計算サービス
class ComplexCalculator {
    public double calculateTotal(double baseAmount) {
        double tax = calculateTax(baseAmount); // 内部メソッド (private/protected) を呼び出す
        return baseAmount + tax;
    }

    protected double calculateTax(double amount) { // 実際の税計算ロジック
        return amount * 0.10; // 10%の税金
    }
}

// テストクラス
@RunWith(PowerMockRunner.class)
@PrepareForTest(ComplexCalculator.class) // 必要に応じて内部メソッドのクラスを指定
public class PartialMockingSpyTest {

    @Test
    public void testPartialMockingTaxCalculation() throws Exception {
        ComplexCalculator calculator = PowerMockito.spy(new ComplexCalculator()); // スパイオブジェクトを作成
        double expectedTax = 50.0; // 税計算をこの値にモックしたい

        // calculateTaxメソッド(ここではprotectedとして実装)の振る舞いをモック化
        // privateメソッドの場合も同様に `PowerMockito.doReturn(expectedTax).when(calculator, "calculateTax", ArgumentMatchers.anyDouble());`
        // のようにメソッド名を文字列で指定して使用できます。
        PowerMockito.doReturn(expectedTax).when(calculator, "calculateTax", ArgumentMatchers.anyDouble());

        double baseAmount = 500.0;
        double actualTotal = calculator.calculateTotal(baseAmount); // calculateTaxはモックされた値を使用

        // baseAmount + expectedTax となることを検証
        Assert.assertEquals(baseAmount + expectedTax, actualTotal, 0.001); // double比較のためデルタを指定
    }
}

タグ: Java JUnit Mockito PowerMock unit testing

10月26日 05:27 投稿