web-dev-qa-db-ja.com

時間依存コードをテストするためのJava System.currentTimeMillis

ホストマシンのシステムクロックを手動で変更する以外に、コードまたはJVM引数を使用して、_System.currentTimeMillis_で表示される現在の時刻をオーバーライドする方法はありますか?

少しの背景:

現在の日付(月の1日、年の1日など)を中心にロジックの多くを展開する多くの会計ジョブを実行するシステムがあります

残念ながら、多くのレガシーコードはnew Date()Calendar.getInstance()などの関数を呼び出します。これらはいずれも最終的に_System.currentTimeMillis_を呼び出します。

テストの目的で、現在、システムクロックを手動で更新して、テストが実行されているとコードが判断する日時を操作する必要があります。

だから私の質問は:

_System.currentTimeMillis_によって返されるものをオーバーライドする方法はありますか?たとえば、JVMに、そのメソッドから戻る前にオフセットを自動的に追加または減算するように指示するには?

前もって感謝します!

116
Mike Clark

I stronglyシステムクロックをいじるのではなく、弾丸を噛み、そのレガシーコードをリファクタリングして、交換可能なクロックを使用することをお勧めします。 理想的にこれは依存性注入で実行する必要がありますが、交換可能なシングルトンを使用した場合でもテスト可能性が得られます。

これは、シングルトンバージョンの検索と置換でほぼ自動化できます。

  • Calendar.getInstance()Clock.getInstance().getCalendarInstance()に置き換えます。
  • new Date()Clock.getInstance().newDate()に置き換えます
  • System.currentTimeMillis()Clock.getInstance().currentTimeMillis()に置き換えます

(必要に応じてなど)

最初の一歩を踏み出したら、シングルトンを少しずつDIに置き換えることができます。

106
Jon Skeet

As Jon Skeetによる

「Joda Timeを使用する」は、ほとんどの場合、「Java.util.Date/CalendarでXを実現するにはどうすればよいか」という質問に対する最良の回答です。

したがって、ここに行きます(すべてのnew Date()new DateTime().toDate()に置き換えたと仮定します)

//Change to specific time
DateTimeUtils.setCurrentMillisFixed(millis);
//or set the clock to be a difference from system time
DateTimeUtils.setCurrentMillisOffset(millis);
//Reset to system time
DateTimeUtils.setCurrentMillisSystem();

インターフェースを持つライブラリをインポートする場合(下記のJonのコメントを参照)、単に Prevayler's Clock を使用できます。これにより、標準インターフェースだけでなく実装も提供されます。完全なjarファイルはわずか96kBなので、銀行を壊すことはありません...

42
Stephen

何らかのDateFactoryパターンを使用するのは良いように見えますが、制御できないライブラリはカバーしていません-System.currentTimeMillisに依存する実装を持つValidationアノテーション@Pastを想像してください(そのようなものがあります)。

そのため、jmockitを使用してシステム時間を直接モックします。

import mockit.Mock;
import mockit.MockClass;
...
@MockClass(realClass = System.class)
public static class SystemMock {
    /**
     * Fake current time millis returns value modified by required offset.
     *
     * @return fake "current" millis
     */
    @Mock
    public static long currentTimeMillis() {
        return INIT_MILLIS + offset + millisSinceClassInit();
    }
}

Mockit.setUpMock(SystemMock.class);

ミリ秒の元のモックされていない値を取得することはできないため、代わりにnanoタイマーを使用します。これは壁時計とは関係ありませんが、ここでは相対時間で十分です。

// runs before the mock is applied
private static final long INIT_MILLIS = System.currentTimeMillis();
private static final long INIT_NANOS = System.nanoTime();

private static long millisSinceClassInit() {
    return (System.nanoTime() - INIT_NANOS) / 1000000;
}

文書化された問題があります。HotSpotでは、何度も電話をかけると時間が正常に戻ります。問題レポートは次のとおりです。 http://code.google.com/p/jmockit/issues/detail?id= 4

これを克服するには、1つの特定のHotSpot最適化をオンにする必要があります-この引数-XX:-Inlineを使用してJVMを実行します。

これは本番には完璧ではないかもしれませんが、特にDataFactoryがビジネスに意味を持たず、テストのためだけに導入された場合、テストには問題なく、アプリケーションには完全に透過的です。別の時間に実行する組み込みのJVMオプションがあればいいのですが、このようなハックなしでは不可能です。

完全なストーリーは、私のブログ投稿にあります: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-Java/

完全な便利なクラスSystemTimeShifterが投稿で提供されています。クラスはテストで使用できます。または、実際のメインクラスの前の最初のメインクラスとして使用して、アプリケーション(またはアプリケーションサーバー全体)を別の時間に実行することもできます。もちろん、これは主に本番環境ではなく、テスト目的のためのものです。

編集2014年7月:JMockitは最近大きく変更され、JMockit 1.0を使用してこれを正しく使用する必要があります(IIRC)。インターフェイスが完全に異なる最新バージョンに間違いなくアップグレードすることはできません。必要なものだけをインライン化することを考えていましたが、新しいプロジェクトではこのことを必要としないので、このことをまったく開発していません。

15
virgo47

Powermock すばらしい動作。 System.currentTimeMillis()のモックに使用しました。

7
ReneS

Aspect-Oriented Programming(AOP、AspectJなど)を使用してSystemクラスを作成し、テストケース内で設定できる定義済みの値を返します。

または、アプリケーションクラスを作成して、System.currentTimeMillis()またはnew Date()への呼び出しを独自の別のユーティリティクラスにリダイレクトします。

織りシステムクラス(Java.lang.*)ただし、少し注意が必要です。rt.jarのオフラインウィービングを実行し、テスト用に別のJDK/rt.jarを使用する必要がある場合があります。

これはBinary weavingと呼ばれ、Systemクラスのウィービングを実行し、それに伴ういくつかの問題を回避するための特別な tools もあります(ブートストラップなど) VMは機能しない可能性があります)

6
mhaller

VMで直接これを行う方法は実際にはありませんが、テストマシンのプログラムでシステム時間を設定するためのすべてのことができます。ほとんどの(すべて?)OSには、これを行うコマンドラインコマンドがあります。

3
Jeremy Raymond

Java 8 EasyMock、Joda Time、およびPowerMockなしのWebアプリケーションでのJUnitテスト目的で現在のシステム時間をオーバーライドする作業方法。

必要なことは次のとおりです。

テストしたクラスで何をする必要があるか

ステップ1

テストしたクラスMyServiceに新しいJava.time.Clock属性を追加し、新しい属性がインスタンス化ブロックまたはコンストラクターを使用してデフォルト値で適切に初期化されることを確認します。

import Java.time.Clock;
import Java.time.LocalDateTime;

public class MyService {
  // (...)
  private Clock clock;
  public Clock getClock() { return clock; }
  public void setClock(Clock newClock) { clock = newClock; }

  public void initDefaultClock() {
    setClock(
      Clock.system(
        Clock.systemDefaultZone().getZone() 
        // You can just as well use
        // Java.util.TimeZone.getDefault().toZoneId() instead
      )
    );
  }
  { 
    initDefaultClock(); // initialisation in an instantiation block, but 
                        // it can be done in a constructor just as well
  }
  // (...)
}

ステップ2

現在の日時を呼び出すメソッドに新しい属性clockを挿入します。たとえば、私の場合、dataaseに保存されている日付がLocalDateTime.now()の前に発生したかどうかのチェックを実行する必要がありましたが、次のようにLocalDateTime.now(clock)に置き換えました。

import Java.time.Clock;
import Java.time.LocalDateTime;

public class MyService {
  // (...)
  protected void doExecute() {
    LocalDateTime dateToBeCompared = someLogic.whichReturns().aDate().fromDB();
    while (dateToBeCompared.isBefore(LocalDateTime.now(clock))) {
      someOtherLogic();
    }
  }
  // (...) 
}

テストクラスで行う必要があること

ステップ3

テストクラスで、模擬クロックオブジェクトを作成し、テストされたメソッドdoExecute()を呼び出す直前にテストされたクラスのインスタンスに挿入し、その後すぐにリセットします。

import Java.time.Clock;
import Java.time.LocalDateTime;
import Java.time.OffsetDateTime;
import org.junit.Test;

public class MyServiceTest {
  // (...)
  private int year = 2017;
  private int month = 2;
  private int day = 3;

  @Test
  public void doExecuteTest() throws Exception {
    // (...) EasyMock stuff like mock(..), expect(..), replay(..) and whatnot

    MyService myService = new MyService();
    Clock mockClock =
      Clock.fixed(
        LocalDateTime.of(year, month, day, 0, 0).toInstant(OffsetDateTime.now().getOffset()),
        Clock.systemDefaultZone().getZone() // or Java.util.TimeZone.getDefault().toZoneId()
      );
    myService.setClock(mockClock); // set it before calling the tested method

    myService.doExecute(); // calling tested method 

    myService.initDefaultClock(); // reset the clock to default right afterwards with our own previously created method

    // (...) remaining EasyMock stuff: verify(..) and assertEquals(..)
    }
  }

デバッグモードで確認すると、2017年2月3日の日付がmyServiceインスタンスに正しく挿入され、比較命令で使用され、initDefaultClock()で現在の日付に正しくリセットされていることがわかります。 。

3
KiriSakow

私の意見では、非侵襲的なソリューションのみが機能します。特に外部ライブラリと大きなレガシーコードベースがある場合、時間をモックアウトする信頼できる方法はありません。

JMockit ...は、制限された数の times でのみ機能します

PowerMock&Co ... clients をSystem.currentTimeMillis()にモックする必要があります。再び侵襲的なオプション。

これから、言及されたjavaagentまたはaopアプローチが透過的に見えるだけですシステム全体。誰かがそれをやったことがあり、そのような解決策を指すことができましたか?

@jarnbjo:javaagentコードの一部を見せていただけますか?

2
user1477398

Linuxを実行している場合、libfaketimeのmasterブランチを使用するか、commit 4ce2835 のテスト時に使用できます。

Javaアプリケーションをモックする時間で環境変数を設定し、ld-preloadingを使用して実行します。

# bash
export FAKETIME="1985-10-26 01:21:00"
export DONT_FAKE_MONOTONIC=1
LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 Java -jar myapp.jar

2番目の環境変数は、Javaアプリケーションで最も重要です。これがなければフリーズします。これは、執筆時点でlibfaketimeのmasterブランチが必要です。

Systemd管理サービスの時刻を変更する場合は、ユニットファイルのオーバーライドに次のコードを追加するだけです。 elasticsearchの場合、これは/etc/systemd/system/elasticsearch.service.d/override.conf

[Service]
Environment="FAKETIME=2017-10-31 23:00:00"
Environment="DONT_FAKE_MONOTONIC=1"
Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1"

`systemctl daemon-reloadを使用してsystemdをリロードすることを忘れないでください

1
faxmodem