概要

消費税率の切り替え日、年度末判定、キャンペーン期間のチェックなど、業務ロジックは「今日の日付」に依存する処理が多くあります。しかし LocalDate.now() をロジック内で直接呼ぶと、テスト時に日付を固定できず、境界値の検証が難しくなります。DateProvider インターフェースを導入して日付の取得元を DI で差し替えられるようにすれば、テストでは固定日付を注入し、本番ではシステム日時を使うという切り分けが自然にできます。この記事では、インターフェース設計、プロダクション実装、テスト用の固定日付プロバイダーを Java 標準 API だけで実装します。

使いどころ

消費税率の変更日(2019/10/1)の前後でロジックの動作を単体テストで検証する

年度末判定(3月31日)のテストを任意の日付で実行できるようにする

環境設定で日付を固定し、過去日付での動作確認やデモ用途に対応する

コード例

DateProvider パターンで日付依存ロジックをテスト可能にする
import java.time.LocalDate;

public class DateProviderDemo {

    interface DateProvider {
        LocalDate getToday();
        LocalDateTime getNow();
    }

    static class SystemDateProvider implements DateProvider {
        @Override
        public LocalDate getToday() {
            return LocalDate.now();
        }
        @Override
        public LocalDateTime getNow() {
            return LocalDateTime.now();
        }
    }

    record FixedDateProvider(LocalDate fixedDate)
            implements DateProvider {
        @Override
        public LocalDate getToday() { return fixedDate; }
        @Override
        public LocalDateTime getNow() {
            return fixedDate.atStartOfDay();
        }
    }

    static class TaxRateService {
        private final DateProvider dateProvider;

        TaxRateService(DateProvider dateProvider) {
            this.dateProvider = dateProvider;
        }

        public int getTaxRate() {
            var today = dateProvider.getToday();
            var boundary = LocalDate.of(2019, 10, 1);
            return !today.isBefore(boundary) ? 10 : 8;
        }
    }

    public static void main(String[] args) {

        var prod = new TaxRateService(new SystemDateProvider());
        System.out.println("現在の税率: " + prod.getTaxRate() + "%");

        var before = new TaxRateService(
            new FixedDateProvider(LocalDate.of(2019, 9, 30)));
        System.out.println("2019/9/30: " + before.getTaxRate() + "%");

        var after = new TaxRateService(
            new FixedDateProvider(LocalDate.of(2019, 10, 1)));
        System.out.println("2019/10/1: " + after.getTaxRate() + "%");
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record を使えばテスト用 FixedDateProvider を1行で定義できる。var と組み合わせてテストコードも簡潔になる。

Java 17
// Java 17: record で簡潔に定義
record FixedDateProvider(LocalDate fixedDate)
        implements DateProvider {
    @Override
    public LocalDate getToday() { return fixedDate; }
    @Override
    public LocalDateTime getNow() {
        return fixedDate.atStartOfDay();
    }
}

Library Comparison

Pure Java (インターフェース DI)日付の差し替えだけなら自前インターフェースが最もシンプル。外部依存なし。DI コンテナなしでも使えるが、注入の管理は手動になる。
java.time.Clockjava.time API との親和性を重視する場合。LocalDate.now(clock) で注入できる。呼び出し側が Clock を意識する必要があり、インターフェースほど意図が明確にならない場面もある。
Mockito (モック)既存コードを変更せずにテストしたい場合。static mock で LocalDate.now() を差し替えられる。テストの可読性が下がりやすい。設計を改善する方が長期的には有利。

注意点

DateProvider の注入を忘れて LocalDate.now() を直接呼ぶメソッドが混在すると、テストの再現性が崩れる。規約で統一すること

テスト用の FixedDateProvider で時刻を固定する場合、atStartOfDay() で 00:00:00 になる点を意識する。時刻精度が必要なテストでは別途対応が必要

ConfigurableDateProvider のような設定ファイル連動型は、設定値の読み込みタイミングに注意。起動時に一度だけ読むか、毎回読むかで挙動が変わる

java.time.Clock を使う方法もあるが、インターフェースの方が呼び出し側の意図が明確になる場面が多い

FAQ

java.time.Clock と DateProvider インターフェースのどちらを使うべきですか?

Clock は標準 API で手軽ですが、DateProvider の方がビジネスロジックとの分離が明確で、意図が伝わりやすいです。

Spring の @Autowired で DateProvider を注入できますか?

可能です。SystemDateProvider を @Component で登録し、テスト時に @MockBean で FixedDateProvider に差し替えます。

DateProvider を導入する単位はクラスごとですか?

日付に依存するサービスクラスのコンストラクタで受け取るのが一般的です。全メソッドに渡す必要はありません。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます