Javaでテストするためのシステム時間のオーバーライド

Javaでテストするためのシステム時間のオーバーライド

1. 概要

このクイックチュートリアルでは、different ways to override the system time for testingに焦点を当てます。

コードには、現在の日付に関するロジックが含まれている場合があります。 おそらく、new Date()Calendar.getInstance()などの一部の関数呼び出しは、最終的にSystem.CurrentTimeMillisを呼び出すことになります。

Java Clockの使用の概要については、to this article hereを参照してください。 または、AspectJを使用するには、hereです。

2. java.timeでクロックを使用する

Java 8 includes an abstract class java.time.Clockjava.timeパッケージは、必要に応じて代替クロックを接続できるようにすることを目的としています。 これにより、独自の実装をプラグインするか、ニーズを満たすためにすでに作成されている実装を見つけることができます。

目標を達成するために、the above library includes static methods to yield special implementations。 そのうちの2つを使用して、不変でスレッドセーフでシリアル化可能な実装を返します。

最初のものはfixedです。 それから、we can obtain a Clock that always returns the same*Instant*, は、テストが現在のクロックに依存していないことを確認します。

これを使用するには、InstantZoneOffsetが必要です。

Instant.now(Clock.fixed(
  Instant.parse("2018-08-22T10:00:00Z"),
  ZoneOffset.UTC))

The second static method is offset。 これでは、クロックは別のクロックをラップし、返されたオブジェクトが指定された期間だけ後または前のインスタンスを取得できるようにします。

言い換えると、it’s possible to simulate running in the future, in the past, or in any arbitrary point in time

Clock constantClock = Clock.fixed(ofEpochMilli(0), ZoneId.systemDefault());

// go to the future:
Clock clock = Clock.offset(constantClock, Duration.ofSeconds(10));

// rewind back with a negative value:
clock = Clock.offset(constantClock, Duration.ofSeconds(-5));

// the 0 duration returns to the same clock:
clock = Clock.offset(constClock, Duration.ZERO);

Durationクラスを使用すると、ナノ秒から数日まで操作できます。 また、期間を無効にすることもできます。これは、長さを無効にしてこの期間のコピーを取得することを意味します。

3. アスペクト指向プログラミングの使用

システム時間をオーバーライドする別の方法は、AOPによるものです。 このアプローチでは、we’re able to weave the System class to return a predefined value which we can set within our test cases

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

これを実装する1つの方法は、AspectJを使用することです。

public aspect ChangeCallsToCurrentTimeInMillisMethod {
    long around():
      call(public static native long java.lang.System.currentTimeMillis())
        && within(user.code.base.pckg.*) {
          return 0;
      }
}

上記の例では、we’re catching every call to System.currentTimeMillis() inside a specified package(この場合はuser.code.base.pckg.)であり、*このイベントが発生するたびにゼロを返します。

この場所で、ミリ秒単位で目的の時間を取得するための独自の実装を宣言できます。

AspectJを使用する利点の1つは、バイトコードレベルで直接動作するため、元のソースコードが機能する必要がないことです。

そのため、再コンパイルする必要はありません。

4. Instant.now()メソッドのモック

Instantクラスを使用して、タイムライン上の瞬間的なポイントを表すことができます。 通常、アプリケーションのイベントタイムスタンプを記録するために使用できます。 このクラスのnow()メソッドを使用すると、UTCタイムゾーンのシステムクロックから現在の瞬間を取得できます。

テスト時に動作を変更するためのいくつかの代替案を見てみましょう。

4.1. now()Clockでオーバーロード

固定のClockインスタンスを使用して、now()メソッドをオーバーロードできます。 Many of the classes in the java.time package have a now() method that takes a Clock parameter。これにより、これが推奨されるアプローチになります。

@Test
public void givenFixedClock_whenNow_thenGetFixedInstant() {
    String instantExpected = "2014-12-22T10:15:30Z";
    Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));

    Instant instant = Instant.now(clock);

    assertThat(instant.toString()).isEqualTo(instantExpected);
}

4.2. PowerMockの使用

さらに、パラメータを送信せずにnow()メソッドの動作を変更する必要がある場合は、PowerMockを使用できます。

@RunWith(PowerMockRunner.class)
@PrepareForTest({ Instant.class })
public class InstantUnitTest {
    @Test
    public void givenInstantMock_whenNow_thenGetFixedInstant() {
        String instantExpected = "2014-12-22T10:15:30Z";
        Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
        Instant instant = Instant.now(clock);
        mockStatic(Instant.class);
        when(Instant.now()).thenReturn(instant);

        Instant now = Instant.now();

        assertThat(now.toString()).isEqualTo(instantExpected);
    }
}

4.3. を使う JMockit

または、JMockitライブラリを使用することもできます。

JMockitは、mocking a static methodの2つの方法を提供します。 1つはMockUpクラスを使用しています。

@Test
public void givenInstantWithJMock_whenNow_thenGetFixedInstant() {
    String instantExpected = "2014-12-21T10:15:30Z";
    Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
    new MockUp() {
        @Mock
        public Instant now() {
            return Instant.now(clock);
        }
    };

    Instant now = Instant.now();

    assertThat(now.toString()).isEqualTo(instantExpected);
}

もう1つは、Expectationsクラスを使用しています。

@Test
public void givenInstantWithExpectations_whenNow_thenGetFixedInstant() {
    Clock clock = Clock.fixed(Instant.parse("2014-12-23T10:15:30.00Z"), ZoneId.of("UTC"));
    Instant instantExpected = Instant.now(clock);
    new Expectations(Instant.class) {
        {
            Instant.now();
            result = instantExpected;
        }
    };

    Instant now = Instant.now();

    assertThat(now).isEqualTo(instantExpected);
}

5. LocalDateTime.now()メソッドのモック

java.timeパッケージのもう1つの便利なクラスは、LocalDateTimeクラスです。 このクラスは、ISO-8601カレンダーシステムのタイムゾーンなしの日付時刻を表します。 このクラスのnow()メソッドを使用すると、デフォルトのタイムゾーンでシステムクロックから現在の日時を取得できます。

以前に見たのと同じ代替手段を使用してそれをモックすることができます。 たとえば、固定Clocknow()をオーバーロードします。

@Test
public void givenFixedClock_whenNow_thenGetFixedLocalDateTime() {
    Clock clock = Clock.fixed(Instant.parse("2014-12-22T10:15:30.00Z"), ZoneId.of("UTC"));
    String dateTimeExpected = "2014-12-22T10:15:30";

    LocalDateTime dateTime = LocalDateTime.now(clock);

    assertThat(dateTime).isEqualTo(dateTimeExpected);
}

6. 結論

この記事では、テストのためにシステム時間をオーバーライドするさまざまな方法を検討しました。 最初に、ネイティブパッケージjava.timeとそのClockクラスを調べました。 次に、アスペクトを適用してSystemクラスを織り込む方法を確認しました。 最後に、InstantクラスとLocalDateTimeクラスでnow()メソッドをモックするためのさまざまな代替案を見ました。

いつものように、コードサンプルはover on GitHubで見つけることができます。