torutkのブログ

ソフトウェア・エンジニアのブログ

JMockitを使ってみる

ユニットテスト対象のクラスが、別なクラスのstaticメソッドを呼んでインスタンスを取得し、そのインスタンスのメソッドを呼び出し、その戻り値によってロジックが左右されるという場合のテストを想定します。

テスト対象クラス

// 気象センサーから取得した気温が30度を超過していたら、
// 異常を報告する気象監視クラス。
package monitor;
import sensor.WeatherSensor;
import sensor.SensorFactory;

public class ThresholdMonitor {
    private WeatherSensor sensor;
    
    public ThresholdMonitor() {
        sensor = SensorFactory.getWeatherSensor();
    }
    
    // 気温を監視し30度を超過していたら異常を報告する。
    public boolean sense() {
        double temperature = sensor.getTemperature();
        if (temperature > 30.0) {
            return false;
        }
        return true;
    }
}

このテスト対象クラスThresholdMonitorは、WeatherSensorインタフェースを実装したインスタンスを、SensorFactoryクラスのstaticメソッドgetWeatherSensorを呼んで取得しています。これらは別パッケージにあるものとします。
パッケージ外に公開されているクラスはこの2つであり、WeatherSensorの実装クラスはパッケージ内部限定です。

JUnitユニットテストを書こうとすると

テスト対象クラス ThresholdMonitorのテストケースを書こうとすると、以下のテストコードになるのですが、ThresholdMonitorの中から呼び出しているWeatherSensorが返す気温はテストコードで指定することができないので、テストを書くことができません。

import org.junit.Test;
import static org.junit.Assert.*;

public class ThresholdMonitorTest {
    @Test
    public void testSenseAlarm() {
        ThresholdMonitor monitor = new ThresholdMonitor();
        // 閾値である30度を超える気温が観測されるようにしたいが、
        // テスト側で制御することができない
        boolean result = monitor.sense();
        // sense結果は実行時環境に依存
        assertEquals(false, result);
    }
}

JMockitを使ってJUnitでテストを書くと

import mockit.Expectations;
import mockit.Mocked;
import org.junit.Test;
imoprt static org.junit.Assert.*;

public class ThresholdMonitorTest {
    // SensorFactoryクラスはこのテストクラス全体でモックとして使用
    @Mocked final SensorFactory unused = null;

    @Test
    public void testSenseAlarm() {
        // SensorFactoryとWeatherSensorのモック定義
        new Expectations() {
            // WeatherSensorのモック定義
            WeatherSensor mockSensor;
            {
                // SensorFactoryクラスのモックは、staticメソッド
                // getWeatherSensorの戻り値としてWeatherSensorの
                // モックmockSensorを返すよう振る舞う
                SensorFactory.getWeatherSensor(); returns(mockSensor);
                // WeatherSensorクラスのインスタンスモック
                // mockSensorはgetTemperatureメソッドの戻り値と
                // して30.5を返すよう振る舞う
                mockSensor.getTemperature(); returns(30.5);
            }
        }
        // ここで生成したThresholdMonitorは、内部で呼び出す
        // SensorFactoryがモックに差し替えられている。
        ThresholdMonitor monitor = new ThresholdMonitor();
        // ここでsenseメソッド内で呼び出すWeatherSensorが
        // モックに差し替えられている。
        boolean result = monitor.sense();
        assertEquals(false, result);
    }
}
コンパイル・実行の注意点

コンパイル時は、junitの他に、jmockit.jarをクラスパスに含めます。

実行時も同様ですが、クラスパス上の指定順番に制約があり、junitのjarファイルよりも先にjmockit.jarを指定する必要があります。順番が違うときは、テスト実行時に以下のメッセージが出ます。

WARNING: JMockit was initialized on demand, which may cause certain tests to fail;
please check the documentation for better ways to get it initialized.

テスト自体は以下の例外発生で停止します。

java.lang.IllegalStateException: Invalid context for the recording of expectations
注意事項:@RunWith(JMockit.class)は使えない

JMockitの最新バージョン(0.999.2)では、JMockitクラスが削除されています。Web上でJMockitを紹介している記事のほとんどでこの@RunWith(JMockit.class)を使う方法を記述しているので、とってもはまりました。
最新バージョンでは、コード上に明記する必要はなく、実行時のクラスパスの順序を守ればいいようです。

NetBeansの場合、プロジェクトのプロパティを開き、左側ペインのカテゴリ欄で[ライブラリ]を選択し、右側ペインで[テストをコンパイル]タブをクリックし、コンパイル時テストライブラリ欄での表示順番でJUnitよりJMockitが上になるように設定します。([上へ移動]/[下へ移動]ボタンで順序を変えられます)