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をコピーしました