web-dev-qa-db-ja.com

Java 8 Clockでクラスを単体テストする

Java 8では、他の多くの_Java.time.Clock_オブジェクトの引数として使用できる_Java.time_が導入され、実際のクロックまたは偽のクロックを注入できるようになりました。たとえば、Clock.fixed()を作成してからInstant.now(clock)を呼び出すと、指定した固定Instantが返されます。これは単体テストに最適です!

ただし、これをどのように使用するのが最善かを考えるのに苦労しています。次のようなクラスがあります。

_public class MyClass {
    private Clock clock = Clock.systemUTC();

    public void method1() {
        Instant now = Instant.now(clock);
        // Do something with 'now'
    }
}
_

次に、このコードの単体テストを行います。 method()を異なる時間でテストできるように、clockを設定して固定時間を生成できるようにする必要があります。明らかに、リフレクションを使用してclockメンバーを特定の値に設定できますが、リフレクションに頼る必要がなければいいでしょう。パブリックsetClock()メソッドを作成できましたが、それは間違っているように感じます。 Clock引数をメソッドに追加したくありません。実際のコードはクロックの受け渡しに関係するべきではないからです。

これを処理するための最良のアプローチは何ですか?これは新しいコードなので、クラスを再編成できます。

編集:明確にするために、単一のMyClassオブジェクトを作成できる必要がありますが、1つのオブジェクトに2つの異なるクロック値を表示できるようにする必要があります(通常のシステムクロックが刻々と変化するように)。そのため、固定クロックをコンストラクターに渡すことはできません。

57
Mike

Jon Skeetの答えとコメントをコードに入れてみましょう。

テスト対象のクラス:

public class Foo {
    private final Clock clock;
    public Foo(Clock clock) {
        this.clock = clock;
    }

    public void someMethod() {
        Instant now = clock.instant();   // this is changed to make test easier
        System.out.println(now);   // Do something with 'now'
    }
}

単体テスト:

public class FooTest() {

    private Foo foo;
    private Clock mock;

    @Before
    public void setUp() {
        mock = mock(Clock.class);
        foo = new Foo(mock);
    }

    @Test
    public void ensureDifferentValuesWhenMockIsCalled() {
        Instant first = Instant.now();                  // e.g. 12:00:00
        Instant second = first.plusSeconds(1);          // 12:00:01
        Instant thirdAndAfter = second.plusSeconds(1);  // 12:00:02

        when(mock.instant()).thenReturn(first, second, thirdAndAfter);

        foo.someMethod();   // string of first
        foo.someMethod();   // string of second
        foo.someMethod();   // string of thirdAndAfter 
        foo.someMethod();   // string of thirdAndAfter 
    }
}
40
Jeff Xiao

実際のコードはクロックを渡すことを考慮すべきではないので、メソッドにClock引数を追加したくありません。

いいえ...しかし、あなたはそれをconstructorパラメータとして考えたいかもしれません。基本的に、クラスには動作するクロックが必要であると言っているので、依存関係になります。他の依存関係と同様に扱い、コンストラクターまたはメソッドを介して注入します。 (個人的にはコンストラクター注入を好みますが、YMMVです。)

何かを自分で簡単に構築できるものと考えるのをやめ、すぐに「別の依存関係」と考え始めるとすぐに、おなじみの手法を使用できます。 (確かに、一般的に依存関係の注入に慣れていると思います。)

42
Jon Skeet

私はここでゲームに少し遅れていますが、クロックを使用することを提案する他の回答に追加する-これは間違いなく機能します。MockitoのdoAnswerを使用すると、テストの進行に合わせて動的に調整できるクロックを作成できます。

コンストラクターでClockを取得するように変更されたこのクラスを想定し、Instant.now(clock)呼び出しでクロックを参照します。

_public class TimePrinter() {
    private final Clock clock; // init in constructor

    // ...

    public void printTheTime() {
        System.out.println(Instant.now(clock));
    }
}
_

次に、テストのセットアップで:

_private Instant currentTime;
private TimePrinter timePrinter;

public void setup() {
   currentTime = Instant.Epoch; // or Instant.now() or whatever

   // create a mock clock which returns currentTime
   final Clock clock = mock(Clock.class);
   when(clock.instant()).doAnswer((invocation) -> currentTime);

   timePrinter = new TimePrinter(clock);
}
_

テストの後半:

_@Test
public void myTest() {
    myObjectUnderTest.printTheTime(); // 1970-01-01T00:00:00Z

    // go forward in time a year
    currentTime = currentTime.plus(1, ChronoUnit.YEARS);

    myObjectUnderTest.printTheTime(); // 1971-01-01T00:00:00Z
}
_

Instant()が呼び出されるたびにcurrentTimeの現在の値を返す関数を常に実行するようにMockitoに指示しています。 Instant.now(clock)clock.instant()を呼び出します。これで、デロリアンよりも早送り、巻き戻し、一般的にタイムトラベルが可能になりました。

25
Rik

まず、@ Jon Skeetの推奨に従って、テスト対象のクラスにClockを確実に挿入します。クラスが1回だけ必要な場合は、単にClock.fixed(...)値を渡します。ただし、クラスが時間の経過とともに異なる動作をする場合(例:それは時間Aで何かをしてから時間Bで何かをする。そしてJavaによって作成されたクロックはimmutable、したがって、ある時点で時間Aを返し、別の時点で時間Bを返すようにテストによって変更することはできません。

受け入れられた答えによると、モックは1つのオプションですが、テストを実装に密接に結び付けます。たとえば、あるコメンターが指摘しているように、テスト対象のクラスがLocalDateTime.now(clock)の代わりにclock.millis()またはclock.instant()を呼び出すとどうなりますか?

もう少し明示的で理解しやすく、モックよりも堅牢な代替アプローチは、Clockの実際の実装を作成することです。つまり、mutable。これにより、テストは必要に応じてそれを挿入および変更できます。これを実装するのは難しくありません。この良い例については、 https://github.com/robfletcher/test-clock をご覧ください。

MutableClock c = new MutableClock(Instant.Epoch, ZoneId.systemDefault());
ClassUnderTest classUnderTest = new ClassUnderTest(c);

classUnderTest.doSomething()
assertTrue(...)

c.instant(Instant.Epoch.plusSeconds(60))

classUnderTest.doSomething()
assertTrue(...)
1
Raman