155

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

少し背景:

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

残念ながら、レガシーコードの多くは、new Date()またはなどの関数を呼び出しますがCalendar.getInstance()、どちらも最終的にはを呼び出しますSystem.currentTimeMillis

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

だから私の質問は:

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

4

12 に答える 12

134

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

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

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

(必要に応じてなど)

その最初のステップを実行したら、シングルトンを一度に少しずつDIに置き換えることができます。

于 2010-01-04T19:43:46.610 に答える
99
于 2014-10-26T07:54:01.903 に答える
44

ジョンスキートが言ったように:

「JodaTimeを使用する」は、ほとんどの場合、「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'sClockを使用できます。フルジャーはわずか96kBなので、銀行を壊してはいけません...

于 2010-01-04T19:48:47.723 に答える
15

いくつかのDateFactoryパターンを使用することは良いように見えますが、制御できないライブラリはカバーしていません-System.currentTimeMillisに依存する実装を持つ検証アノテーション@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);

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

// 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=43

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

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

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

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

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

于 2013-01-02T10:05:24.843 に答える
8

Powermockはうまく機能します。ただそれをモックに使用しましたSystem.currentTimeMillis()

于 2010-04-03T18:27:12.613 に答える
6

アスペクト指向プログラミング(AOP、たとえばAspectJ)を使用して、システムクラスを織り込み、テストケース内で設定できる事前定義された値を返します。

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

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

これはバイナリウィービングと呼ばれ、システムクラスのウィービングを実行し、それに関するいくつかの問題を回避するための特別なツールもあります(たとえば、VMのブートストラップが機能しない場合があります)

于 2010-01-04T19:49:01.293 に答える
6

A working way to override current system time for JUnit testing purposes in a Java 8 web application with EasyMock, without Joda Time, and without PowerMock.

Here's what you need to do:

What needs to be done in the tested class

Step 1

Add a new java.time.Clock attribute to the tested class MyService and make sure the new attribute will be initialized properly at default values with an instantiation block or a constructor:

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
  }
  // (...)
}

Step 2

Inject the new attribute clock into the method which calls for a current date-time. For instance, in my case I had to perform a check of whether a date stored in dataase happened before LocalDateTime.now(), which I remplaced with LocalDateTime.now(clock), like so:

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();
    }
  }
  // (...) 
}

What needs to be done in the test class

Step 3

In the test class, create a mock clock object and inject it into the tested class's instance just before you call the tested method doExecute(), then reset it back right afterwards, like so:

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(..)
    }
  }

Check it in debug mode and you will see the date of 2017 Feb 3 has been correctly injected into myService instance and used in the comparison instruction, and then has been properly reset to current date with initDefaultClock().

于 2017-08-21T14:46:34.183 に答える
3

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

JMockit...は制限された回数だけ動作します

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

このことから、前述のjavaagentまたはaopアプローチがシステム全体に対して透過的であることがわかります。誰かがそれをして、そのような解決策を指摘することができますか?

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

于 2012-06-23T22:46:32.433 に答える
3

Here is an example using PowerMockito. Also there is an example with new Date().
More details about mocking system classes.

@RunWith(PowerMockRunner.class)
@PrepareForTest(LegacyClass.class)
public class SystemTimeTest {
    
    private final Date fakeNow = Date.from(Instant.parse("2010-12-03T10:15:30.00Z"));

    @Before
    public void init() {
        PowerMockito.mockStatic(System.class);
        PowerMockito.when(System.currentTimeMillis()).thenReturn(fakeNow.getTime());
        System.out.println("Fake currentTimeMillis: " + System.currentTimeMillis());
    }

    @Test
    public void legacyClass() {
        new LegacyClass().methodWithCurrentTimeMillis();
    }

}

Some legacy class you are testing:

class LegacyClass {

    public void methodWithCurrentTimeMillis() {
        long now = System.currentTimeMillis();
        System.out.println("LegacyClass System.currentTimeMillis() is " + now);
    }

}

Console output

Fake currentTimeMillis: 1291371330000
LegacyClass System.currentTimeMillis() is 1291371330000
于 2021-04-02T14:28:51.453 に答える
2

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

于 2010-01-04T19:43:30.757 に答える
2

If you're running Linux, you can use the master branch of libfaketime, or at the time of testing commit 4ce2835.

Simply set the environment variable with the time you'd like to mock your java application with, and run it using 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

The second environment variable is paramount for java applications, which otherwise would freeze. It requires the master branch of libfaketime at the time of writing.

If you'd like to change the time of a systemd managed service, just add the following to your unit file overrides, e.g. for elasticsearch this would be /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"

Don't forget to reload systemd using `systemctl daemon-reload

于 2017-11-03T09:41:45.767 に答える
-1

If you want to mock the method having System.currentTimeMillis() argument then you can pass anyLong() of Matchers class as an argument.

P.S. I am able to run my test case successfully using the above trick and just to share more details about my test that I am using PowerMock and Mockito frameworks.

于 2015-08-12T10:12:31.147 に答える