java-recipes

ホーム 日付・時刻 › D-09

D-09: DateProvider パターン(テスト容易な日付注入)

ビジネスロジックの中で LocalDate.now() を直接呼ぶと、 「今日が増税日の前日のときに正しく動くか」「年度末判定が 3/31 で正しく true になるか」といったテストが書けません。 DateProvider インターフェースを DI(依存性注入)することで、テスト時に任意の日付を注入できるようにします。

いつ使うか

  • 消費税率・キャンペーン適用期間・有効期限など、「今日の日付」に依存するビジネスロジックをテストしたいとき
  • バッチ処理で「処理基準日」を設定ファイルから注入してテストしたいとき
  • 日付処理を含むクラスの単体テストで、特定の日付を固定して境界値テストを行いたいとき

問題のあるコード vs DateProvider パターン

❌ テストしにくいコード

public int getTaxRate() {
    // now() を直接呼ぶとテスト不可
    LocalDate today = LocalDate.now();
    if (!today.isBefore(
            LocalDate.of(2019, 10, 1))) {
        return 10;
    }
    return 8;
}

✅ DateProvider パターン

public int getTaxRate() {
    // dateProvider 経由で取得
    LocalDate today =
        dateProvider.getToday();
    if (!today.isBefore(
            LocalDate.of(2019, 10, 1))) {
        return 10;
    }
    return 8;
}

サンプルコード

DateProviderSample.java
import java.time.LocalDate;
import java.time.LocalDateTime;

public class DateProviderSample {

    // ① DateProvider インターフェース(日付に依存するロジックを切り離す)
    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();
        }
    }

    // ③ テスト用固定日付プロバイダー(指定した日付を常に返す)
    static class FixedDateProvider implements DateProvider {
        private final LocalDate fixedDate;
        private final LocalDateTime fixedDateTime;

        FixedDateProvider(LocalDate fixedDate) {
            this.fixedDate = fixedDate;
            this.fixedDateTime = fixedDate.atStartOfDay();
        }

        @Override
        public LocalDate getToday() {
            return fixedDate;
        }

        @Override
        public LocalDateTime getNow() {
            return fixedDateTime;
        }
    }

    // ④ 日付に依存するビジネスロジック(DateProvider を DI で受け取る)
    static class TaxRateService {
        private final DateProvider dateProvider;

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

        // 消費税率を返す(2019/10/01 以降は10%、それより前は8%)
        public int getTaxRate() {
            LocalDate today = dateProvider.getToday();
            LocalDate taxRaiseDate = LocalDate.of(2019, 10, 1);
            if (!today.isBefore(taxRaiseDate)) {
                return 10;
            }
            return 8;
        }

        // 年度末(3月31日)かどうかを判定
        public boolean isEndOfFiscalYear() {
            LocalDate today = dateProvider.getToday();
            return today.getMonthValue() == 3 && today.getDayOfMonth() == 31;
        }
    }

    public static void main(String[] args) {
        // プロダクション利用(実際のシステム日時を使う)
        TaxRateService prodService = new TaxRateService(new SystemDateProvider());
        System.out.println("現在の消費税率: " + prodService.getTaxRate() + "%");

        // テスト:増税前日(2019/9/30)を固定して動作確認
        DateProvider beforeProvider = new FixedDateProvider(LocalDate.of(2019, 9, 30));
        TaxRateService beforeService = new TaxRateService(beforeProvider);
        System.out.println("2019/9/30 の税率: " + beforeService.getTaxRate() + "%"); // → 8%

        // テスト:増税日当日(2019/10/1)を固定して動作確認
        DateProvider afterProvider = new FixedDateProvider(LocalDate.of(2019, 10, 1));
        TaxRateService afterService = new TaxRateService(afterProvider);
        System.out.println("2019/10/1 の税率: " + afterService.getTaxRate() + "%");  // → 10%

        // テスト:年度末判定
        DateProvider fiscalEndProvider = new FixedDateProvider(LocalDate.of(2024, 3, 31));
        TaxRateService fiscalService = new TaxRateService(fiscalEndProvider);
        System.out.println("2024/3/31 は年度末: " + fiscalService.isEndOfFiscalYear()); // → true
    }
}

Java 8 では FixedDateProvider をクラスで実装します。フィールドに fixedDate と fixedDateTime を持ち、コンストラクターで両方を初期化します。

よくあるミス・注意点

⚠️ LocalDate.now() を直接呼ぶとテストができない

ビジネスロジックの中で直接 LocalDate.now() を呼ぶと、テストを実行する「今日の日付」に依存してしまいます。 たとえば 2019/10/01 より前に実装してテストが通っていたコードが、 増税後の日付でテストを実行すると失敗する可能性があります。 DateProvider を DI することで、テストで任意の日付を注入できるようになります。

⚠️ ConfigurableDateProvider の null 処理に注意

設定ファイルに日付が設定されていない場合(null または空文字)は実時刻を使うという設計にすると、 本番環境で意図せず固定日付が有効になるリスクを避けられます。 ただし、null を許容する設計は NullPointerException の原因にもなるため、 初期化時に状態を確定させ、getToday() / getNow() 内では null チェックを確実に行いましょう。

⚠️ Java 17 の record は不変オブジェクト

record FixedDateProvider(LocalDate fixedDate) はフィールドが final で変更できません。一度生成したら日付を変えることはできないため、 テストケースごとに新しいインスタンスを生成してください。 これは record の特性を活かした正しい使い方です。

テストする観点

  • 増税前日(2019/9/30)で getTaxRate() が 8 を返すこと(境界値)
  • 増税当日(2019/10/1)で getTaxRate() が 10 を返すこと(境界値)
  • 年度末(3/31)で isEndOfFiscalYear() が true を返すこと
  • 年度末以外(3/30、4/1)で isEndOfFiscalYear() が false を返すこと(境界値)
  • ConfigurableDateProvider に有効な日付文字列を渡したとき、その日付が返ること
  • ConfigurableDateProvider に null または空文字を渡したとき、システム日時が返ること

GitHub でソースコードを見る →