Java Tips | 日付・時間:テスト用日時固定

Java Java
スポンサーリンク
スポンサーリンク

なぜ「テスト用日時固定」ユーティリティが必要になるのか

日付・時間に依存するロジックは、業務システムのど真ん中にいます。
「締め切りを過ぎているか」「有効期限内か」「月末かどうか」「営業日かどうか」。
こういう処理をテストするときに、LocalDate.now()Instant.now() をそのまま使っていると、テストがとてもやりづらくなります。

今日が何日かによってテスト結果が変わる。
月末のテストをしたいのに、実際のカレンダーが月末になるまで待たないといけない。
「1年後」「10年後」の挙動を確認したいのに、現実の時間を進めることはできない。

これを一気に解決する考え方が、「テスト用に日時を固定する」ユーティリティです。
“システムにとっての今”を、テストコード側から自由に決められるようにします。


基本の考え方は「Clock を固定する」

直接 now() を呼ぶのではなく、Clock を渡す

まず大前提として、業務ロジックの中で LocalDate.now()Instant.now() を直接呼ばない、というルールを置きます。
代わりに、「Clock を受け取って、その Clock から“今”を取得する」ようにします。

例えば、「今日が締め切りを過ぎているか」を判定するクラスを考えます。

import java.time.Clock;
import java.time.LocalDate;

public class DeadlineService {

    private final Clock clock;

    public DeadlineService(Clock clock) {
        this.clock = clock;
    }

    public boolean isExpired(LocalDate deadline) {
        LocalDate today = LocalDate.now(clock);
        return today.isAfter(deadline);
    }
}
Java

本番では、普通のシステム時計を渡します。

DeadlineService service = new DeadlineService(Clock.systemDefaultZone());
Java

ここまでが「Clock差し替え」の土台です。
この土台があるからこそ、「テスト用日時固定」が生きてきます。


テスト用日時固定の主役:Clock.fixed

任意の日時を「今」として固定する

テストでやりたいことはシンプルです。
「今が 2025-03-31 の 10:00 だと仮定して、このロジックを動かしたい」。
これを実現してくれるのが Clock.fixed です。

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

public class DeadlineServiceTest {

    public static void main(String[] args) {
        Instant fixedInstant = Instant.parse("2025-03-31T01:00:00Z");
        ZoneId zone = ZoneId.of("Asia/Tokyo");
        Clock fixedClock = Clock.fixed(fixedInstant, zone);

        DeadlineService service = new DeadlineService(fixedClock);

        LocalDate deadline1 = LocalDate.of(2025, 3, 30);
        LocalDate deadline2 = LocalDate.of(2025, 3, 31);
        LocalDate deadline3 = LocalDate.of(2025, 4, 1);

        System.out.println(service.isExpired(deadline1)); // true を期待
        System.out.println(service.isExpired(deadline2)); // 仕様に応じて true/false を確認
        System.out.println(service.isExpired(deadline3)); // false を期待
    }
}
Java

ここで重要なのは、「テストコード側で“今”を完全にコントロールしている」ということです。
実際のカレンダーが何日であろうと、テストは常に「2025-03-31 の世界」で動きます。


「テスト用日時固定」ユーティリティをひとまとめにする

毎回 Clock.fixed を書かないための小さなヘルパー

テストコードのあちこちで Clock.fixed(Instant.parse(...), ZoneId.of(...)) と書くのは、少し面倒です。
そこで、「テスト用日時固定」を行う小さなユーティリティを用意しておくと便利です。

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

public class TestClocks {

    public static Clock fixedAt(LocalDateTime dateTime, ZoneId zone) {
        Instant instant = dateTime.atZone(zone).toInstant();
        return Clock.fixed(instant, zone);
    }

    public static Clock fixedAtTokyo(LocalDateTime dateTime) {
        ZoneId tokyo = ZoneId.of("Asia/Tokyo");
        return fixedAt(dateTime, tokyo);
    }
}
Java

使い方の例です。

import java.time.LocalDateTime;

public class DeadlineServiceTest2 {

    public static void main(String[] args) {
        Clock clock = TestClocks.fixedAtTokyo(
                LocalDateTime.of(2025, 3, 31, 10, 0)
        );

        DeadlineService service = new DeadlineService(clock);

        // 以降は同じ
    }
}
Java

ここで深掘りしたいのは、「テストコードの可読性」です。
fixedAtTokyo(2025-03-31 10:00) のように書けると、
「このテストはどんな日時を前提にしているのか」が一目で分かります。


「時間が進むテスト」と「時間が止まったテスト」

fixed は「時間が止まった世界」を作る

Clock.fixed は、その名の通り「時間が止まった Clock」です。
Instant.now(clock) を何度呼んでも、同じ値が返ってきます。

これは、「ある瞬間における判定」をテストするには最高ですが、
「1時間後」「1日後」の挙動をテストしたいときには少し工夫が必要です。

「時間を手動で進める」テスト用 Clock を作る

少し応用として、「テストコードから時間を進められる Clock」を自作することもできます。
初心者向けに、シンプルなイメージだけ示します。

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;

public class MutableClock extends Clock {

    private Instant current;
    private final ZoneId zone;

    public MutableClock(Instant initial, ZoneId zone) {
        this.current = initial;
        this.zone = zone;
    }

    @Override
    public ZoneId getZone() {
        return zone;
    }

    @Override
    public Clock withZone(ZoneId zone) {
        return new MutableClock(current, zone);
    }

    @Override
    public Instant instant() {
        return current;
    }

    public void plusSeconds(long seconds) {
        current = current.plusSeconds(seconds);
    }
}
Java

テストでは、こう使えます。

MutableClock clock = new MutableClock(
        Instant.parse("2025-03-31T00:00:00Z"),
        ZoneId.of("Asia/Tokyo")
);

DeadlineService service = new DeadlineService(clock);

// 0時の世界でテスト
// ...

clock.plusSeconds(3600); // 1時間進める

// 1時の世界でテスト
// ...
Java

ここでのポイントは、「テスト用日時固定」をさらに発展させて、“テスト用時間操作”までできるようにしていることです。
本格的なシステムでは、こうしたテクニックがかなり効いてきます。


アプリ全体で「テスト用日時固定」をどう扱うか

共通の Clock プロバイダを持つ設計

以前の「Clock差し替え」と同じく、アプリ全体で使う Clock を一か所から提供する設計にしておくと、
テスト用日時固定がとても楽になります。

例えば、こんなクラスを用意します。

import java.time.Clock;

public class AppClock {

    private static Clock clock = Clock.systemDefaultZone();

    public static Clock get() {
        return clock;
    }

    public static void set(Clock newClock) {
        clock = newClock;
    }

    public static void reset() {
        clock = Clock.systemDefaultZone();
    }
}
Java

業務コード側では、こう書きます。

import java.time.LocalDate;

public class SomeService {

    public LocalDate today() {
        return LocalDate.now(AppClock.get());
    }
}
Java

テストでは、次のように「テスト用日時固定」を行えます。

AppClock.set(TestClocks.fixedAtTokyo(
        LocalDateTime.of(2025, 3, 31, 10, 0)
));

// テスト実行

AppClock.reset(); // 終わったら元に戻す
Java

ここで深掘りしたいのは、「テスト用日時固定を“アプリ全体の設定”として扱う」という感覚です。
これにより、「このテストスイートは全部 2025-03-31 の世界で動かす」といったことが簡単にできます。


セキュリティ・運用の観点から見た「テスト用日時固定」

「未来日」「過去日」のバグを事前に潰せる

有効期限、長期保存、ログの保持期間、契約期間など、
日付・時間に関するバグは、運用を続けてから初めて表面化することが多いです。

テスト用日時固定をきちんと用意しておけば、
「1年後」「5年後」「有効期限ギリギリ」「期限切れ直後」といった状態を、
テスト環境で安全に再現できます。

これは、「本番でしか起きない日付バグ」を事前に潰すための、かなり強力な武器です。

「システム時刻の変更」に依存しないテスト

運用上、サーバの時刻をいじることは極力避けるべきです。
テストのために本番相当環境の時刻を変える、というのは論外です。

テスト用日時固定ユーティリティを使えば、
サーバの時刻を一切いじらずに、「任意の日時の世界」をテスト内だけで作れます。
これは、セキュリティ・運用の観点から見ても、とても健全なやり方です。


まとめ:テスト用日時固定ユーティリティで身につけてほしい感覚

テスト用日時固定ユーティリティは、「システムにとっての“今”を、テストコード側から自由に決める」ための仕組みです。
その中心にあるのは、次のような考え方です。

業務ロジックは now() を直接呼ばず、必ず Clock を受け取る。
テストでは Clock.fixed や自作のテスト用 Clock を使って、「今」を固定・操作する。
共通の Clock プロバイダ(AppClock など)を用意し、アプリ全体で一貫した“今”を扱う。
未来日・過去日・境界日(締め切り日・月末など)を、テストで何度でも再現できるようにする。

もしあなたのテストコードが、「今日が何日かによって成功したり失敗したりする」状態になっているなら、
それはまさに「テスト用日時固定」を導入するサインです。

一度、日付・時間に依存するクラスを洗い出して、
「Clock を受け取るように書き換えられないか」「テスト用 Clock を差し込めないか」を眺めてみてください。

そこから先は、テストが安定し、将来の運用年数を見据えた“強い”日付・時間設計に、自然とつながっていきます。

タイトルとURLをコピーしました