単体テスト実行時に任意のシステム日付でテストをしたいというケースで便利なのが、jmockitを使ったシステム時刻のモック化です。new Date()等で日付をセットしている箇所の日付を、jmockitを使ってテスト実行時に任意の日付に固定化するには以下のようなコードとテストケースで実現できます。
日付をモックするクラス
System.currentTimeMillis()をモックします。
import mockit.Mock; import mockit.MockUp; import java.util.Date; public class CurrentTimeMock extends MockUp{ Date mockTime; public CurrentTimeMock(Date mockTime) { this.mockTime = mockTime; } @Mock public long currentTimeMillis() { return mockTime.getTime(); } }
日付をモックしたいテストケース
import org.joda.time.DateTime; import org.junit.Test; import java.util.Date; public class SampleTest { @Test public void 日付をモックしたテスト() { new CurrentTimeMock(new DateTime(2014,5,1,0,0).toDate()); System.out.println("mock date " + new Date()); // ここで↑で指定した時間がセットされます。 } }
CurrentTimeMockのコンストラクタで指定した時刻をSystem.currentTimeMillis()が返すようになるので、プロダクションコード内で日付を生成した場合、CurrentTimeMockのコンストラクタで指定した時刻が返るようになります。
あとは日付が固定化される前提でテストを書いていけば良い訳ですが、このモックを使ったテストが増えると全件テスト等を流した際に、途中からモックが有効にならず現在時刻が返される現象が発生しました。実行環境は以下の通りです。
- OS
Ubuntu 14.10 64bit - Java
Java version: 1.8.0_05, vendor: Oracle Corporation - jmockit
1.8
上記環境で↓の処理を走らせると100000回に行く前にモックが無効化されます。
@Test public void 日付をモックしたテスト() { new CurrentTimeMock(new DateTime(2014,5,1,0,0).toDate()); for (int i = 1; i < 100000; i++) { if (!new Date().equals(new DateTime(2014, 5, 1, 0, 0).toDate())) { System.out.println("count " + i + " date " + new Date()); fail(); } } }
この現象自体はjmockitのissuesにも掲載されてます。
Stubbing System.currentTimeMillis() only works for a limited number of invocations
どうもJVMによって挙動が変わるようです。(IBMのJVMだと出ないとか?)
記載されてる報告みながら、試しにテストを実行する際のVMオプションに-Xintを指定してJITコンパイラを無効化しインタプリタ実行にすると、途中でモックされなくなる現象は回避されるようになりました。
おそらくモック処理のコードの呼び出しが一定数超えて、JVMのJITコンパイラの最適化処理が行われた結果、モックできなくなってるんじゃないかと思います。今回はSystem.currentTimeMillis()でしたが、どうもstaticかつnative宣言されたメソッドについては同様の現象が発生します。
(ちなみにClient VMにして、-XX:-Inlineオプションを付加しインライン展開を無効化してみましたが同様の現象が発生しました。)
とりあえず上記のような現象が発生するようになったらインタプリタ実行をするようにすれば現象は回避可能です。ただ、JITコンパイラによる最適化は行われず随時インタプリタ実行されるのでテストの実行時間自体は遅くなります…。