🐕

C#で日付をテストする際のTips (明日かどうかと通貨休日かどうかを判定する)

2025/01/09に公開

明日かどうか」「通貨休日かどうか」を判定する機能をテストするときに、現在日付を自由に制御(固定)してテストするための実装パターンを解説します。テストをする際に「日付や時刻が動的に変わってしまうため再現性が保てない」「明日かどうかの判定が日付によって失敗する」「休日が正しく判定できるか日付に左右される」といった問題に対処する上で役立ちます。

ここでは例として、以下のような判定メソッドを想定します。

public class HolidayChecker
{
    // 例: 通貨休日の日付一覧(シンプルにDateTime配列で定義)
    // 実際はファイル/DBや外部APIなどから取得するケースもあります。
    private static readonly DateTime[] CurrencyHolidays = new[]
    {
        new DateTime(2025, 1, 1),
        new DateTime(2025, 2, 11),
        new DateTime(2025, 12, 25)
        // ...等、実際の通貨休日リスト
    };

    // 現在日付を取得する箇所を抽象化しておく(ここがポイント)
    private readonly Func<DateTime> _getCurrentDate;

    public HolidayChecker(Func<DateTime> getCurrentDate)
    {
        _getCurrentDate = getCurrentDate;
    }

    /// <summary>
    /// 指定した日付が明日かどうかを判定する
    /// </summary>
    public bool IsTomorrow(DateTime targetDate)
    {
        // "今日"が何日かを取得
        var today = _getCurrentDate().Date;
        return targetDate.Date == today.AddDays(1);
    }

    /// <summary>
    /// 指定した日付が通貨休日かどうかを判定する
    /// </summary>
    public bool IsCurrencyHoliday(DateTime targetDate)
    {
        return CurrencyHolidays.Any(h => h.Date == targetDate.Date);
    }
}

テストでは、このHolidayCheckerの動作確認を、**あらゆる「今日の日付」**でシミュレーションしたいとします。
しかし、C#のコードで DateTime.NowDateTime.UtcNow を直接使ってしまうと、「今日」の日付が変わればテスト結果も変わってしまいます。そこで、現在日付をモック(仮想)にするための4つの実装パターンをご紹介します。


1. NodaTime.Testing を使う方法

概要

NodaTime は高機能な日時ライブラリで、テスト用に「現在時刻を固定」できる FakeClock クラスを提供しています。タイムゾーンの扱いが多いプロジェクトでは特に有効です。

実装例

1.1 HolidayChecker 側の対応

NodaTime を使う場合、DateTime.Now をそのまま呼び出すのではなく、NodaTime で取得した日時オブジェクト(InstantZonedDateTime)を使う設計にします。
しかしここでは、簡易的に「Func<DateTime>を注入する」構成のままテストだけ NodaTime 側を利用する方法を示します。

using NodaTime;
using NodaTime.Testing;

public class HolidayChecker
{
    private static readonly DateTime[] CurrencyHolidays = { /* ...省略... */ };
    private readonly Func<DateTime> _getCurrentDate;

    public HolidayChecker(Func<DateTime> getCurrentDate)
    {
        _getCurrentDate = getCurrentDate;
    }

    public bool IsTomorrow(DateTime targetDate)
    {
        var today = _getCurrentDate().Date;
        return targetDate.Date == today.AddDays(1);
    }

    public bool IsCurrencyHoliday(DateTime targetDate)
    {
        return CurrencyHolidays.Any(h => h.Date == targetDate.Date);
    }
}

1.2 テストコード

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_NodaTimeFakeClock()
    {
        // 2025-01-08T00:00:00Z を固定したFakeClockを用意(UTC)
        var fixedClock = new FakeClock(Instant.FromUtc(2025, 1, 8, 0, 0));

        // 「今日」をNodaTimeのUTCインスタントからDateTimeに変換して注入
        Func<DateTime> getNow = () => fixedClock
            .GetCurrentInstant()
            .InZone(DateTimeZoneProviders.Tzdb["UTC"]) // タイムゾーン
            .ToDateTimeUnspecified();

        var checker = new HolidayChecker(getNow);

        // 「今日」が2025-01-08なので、明日は2025-01-09
        Assert.IsTrue(checker.IsTomorrow(new DateTime(2025, 1, 9)));
        Assert.IsFalse(checker.IsTomorrow(new DateTime(2025, 1, 8)));
    }

    [Test]
    public void IsCurrencyHoliday_NodaTimeFakeClock()
    {
        // 同じく2025-01-08を現在時刻とする
        var fixedClock = new FakeClock(Instant.FromUtc(2025, 1, 8, 0, 0));
        Func<DateTime> getNow = () => fixedClock
            .GetCurrentInstant()
            .InZone(DateTimeZoneProviders.Tzdb["UTC"])
            .ToDateTimeUnspecified();

        var checker = new HolidayChecker(getNow);

        // 通貨休日一覧に2025-01-01が含まれている想定
        // (今日が1/8 でも、通貨休日の判定自体は targetDate に対して行う)
        Assert.IsTrue(checker.IsCurrencyHoliday(new DateTime(2025, 1, 1)));
        Assert.IsFalse(checker.IsCurrencyHoliday(new DateTime(2025, 1, 8)));
    }
}

メリット

  • タイムゾーンを扱うプロジェクトに最適。
  • FakeClock により、時間の経過もシミュレートしやすい。
  • 日時計算が複雑な環境でも大活躍。

2. SystemWrapper を使用する方法

概要

SystemWrapper は .NET の標準クラス (DateTime, File, Directory など) をラップし、テスト容易性を高めるライブラリです。DateTimeWrapper を使えば DateTime.Now の動作をモック可能となり、現在日付を自由に差し替えられます。

実装例

2.1 HolidayChecker 側の対応

DateTime.Now をラップするクラス(IDateTime)を使う実装に変更します。

using SystemWrapper.Interfaces;
using SystemWrapper;

public class HolidayChecker
{
    private static readonly DateTime[] CurrencyHolidays = { /* ...省略... */ };
    private readonly IDateTime _dateTime; // SystemWrapper で提供されるインターフェース

    public HolidayChecker(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }

    public bool IsTomorrow(DateTime targetDate)
    {
        var today = _dateTime.Now.Date;
        return targetDate.Date == today.AddDays(1);
    }

    public bool IsCurrencyHoliday(DateTime targetDate)
    {
        return CurrencyHolidays.Any(h => h.Date == targetDate.Date);
    }
}

2.2 テストコード

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_SystemWrapper()
    {
        // IDateTimeをMoqなどでモックする
        var mockDateTime = new Mock<IDateTime>();
        // 「今日」を2025-01-08に固定
        mockDateTime.Setup(dt => dt.Now).Returns(new DateTime(2025, 1, 8));

        var checker = new HolidayChecker(mockDateTime.Object);

        Assert.IsTrue(checker.IsTomorrow(new DateTime(2025, 1, 9)));
        Assert.IsFalse(checker.IsTomorrow(new DateTime(2025, 1, 8)));
    }

    [Test]
    public void IsCurrencyHoliday_SystemWrapper()
    {
        var mockDateTime = new Mock<IDateTime>();
        // 「今日」は 2025-01-08だが、通貨休日の判定は targetDate が対象
        mockDateTime.Setup(dt => dt.Now).Returns(new DateTime(2025, 1, 8));

        var checker = new HolidayChecker(mockDateTime.Object);

        Assert.IsTrue(checker.IsCurrencyHoliday(new DateTime(2025, 1, 1)));
        Assert.IsFalse(checker.IsCurrencyHoliday(new DateTime(2025, 1, 10)));
    }
}

メリット

  • .NET 標準クラスをそのままラップしているため、既存コードを大きく変えずに導入しやすい。
  • ファイル操作や他の標準クラスもモックしやすくなり、テストが広範囲にしやすい。

3. インターフェース + 依存性注入(DI) を使う方法

概要

自前で「現在時刻を取得するインターフェース (IClock など)」を用意し、DI(依存性注入)を行う最もオーソドックスなパターンです。
フレームワークを問わず導入しやすく、テストでの置き換えも自由度が高いです。

実装例

3.1 インターフェースの定義

public interface IClock
{
    DateTime Now { get; }
}

3.2 実装クラス(本番用)

public class SystemClock : IClock
{
    public DateTime Now => DateTime.Now;
}

3.3 HolidayChecker 側の対応

public class HolidayChecker
{
    private static readonly DateTime[] CurrencyHolidays = { /* ...省略... */ };
    private readonly IClock _clock;

    public HolidayChecker(IClock clock)
    {
        _clock = clock;
    }

    public bool IsTomorrow(DateTime targetDate)
    {
        var today = _clock.Now.Date;
        return targetDate.Date == today.AddDays(1);
    }

    public bool IsCurrencyHoliday(DateTime targetDate)
    {
        return CurrencyHolidays.Any(h => h.Date == targetDate.Date);
    }
}

3.4 テストコード

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_UsingInterface()
    {
        // IClock をモック
        var mockClock = new Mock<IClock>();
        // 「今日」を2025-01-08に固定
        mockClock.Setup(c => c.Now).Returns(new DateTime(2025, 1, 8));

        var checker = new HolidayChecker(mockClock.Object);

        Assert.IsTrue(checker.IsTomorrow(new DateTime(2025, 1, 9)));
        Assert.IsFalse(checker.IsTomorrow(new DateTime(2025, 1, 8)));
    }

    [Test]
    public void IsCurrencyHoliday_UsingInterface()
    {
        var mockClock = new Mock<IClock>();
        mockClock.Setup(c => c.Now).Returns(new DateTime(2025, 1, 8));

        var checker = new HolidayChecker(mockClock.Object);

        Assert.IsTrue(checker.IsCurrencyHoliday(new DateTime(2025, 1, 1)));
        Assert.IsFalse(checker.IsCurrencyHoliday(new DateTime(2025, 1, 10)));
    }
}

メリット

  • フレームワークに依存しないシンプルで拡張性の高い設計。
  • 日時取得以外にも、同じ考え方で様々な外部依存をモックしやすくなる。

4. 簡易的な方法(Func<DateTime> を利用)

概要

小規模なプロジェクトや、素早く実装したいケース」では、引数に Func<DateTime> を受け取り、それを「現在日付取得ロジック」として使う方法があります。上記で使ったコード例のように、最小限の改修で済むのがメリットです。

実装例

4.1 HolidayChecker 側の対応(すでに解説した最初の例)

public class HolidayChecker
{
    private static readonly DateTime[] CurrencyHolidays = { /* ...省略... */ };
    private readonly Func<DateTime> _getCurrentDate;

    public HolidayChecker(Func<DateTime> getCurrentDate)
    {
        _getCurrentDate = getCurrentDate;
    }

    public bool IsTomorrow(DateTime targetDate)
    {
        var today = _getCurrentDate().Date;
        return targetDate.Date == today.AddDays(1);
    }

    public bool IsCurrencyHoliday(DateTime targetDate)
    {
        return CurrencyHolidays.Any(h => h.Date == targetDate.Date);
    }
}

4.2 テストコード

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_Func()
    {
        // 今日を2025-01-08に固定
        Func<DateTime> fixedNow = () => new DateTime(2025, 1, 8);

        var checker = new HolidayChecker(fixedNow);

        Assert.IsTrue(checker.IsTomorrow(new DateTime(2025, 1, 9)));
        Assert.IsFalse(checker.IsTomorrow(new DateTime(2025, 1, 8)));
    }

    [Test]
    public void IsCurrencyHoliday_Func()
    {
        Func<DateTime> fixedNow = () => new DateTime(2025, 1, 8);
        var checker = new HolidayChecker(fixedNow);

        Assert.IsTrue(checker.IsCurrencyHoliday(new DateTime(2025, 1, 1)));
        Assert.IsFalse(checker.IsCurrencyHoliday(new DateTime(2025, 1, 10)));
    }
}

メリット

  • コード量が少なく、最小限の実装で日付を固定できる。
  • 大規模な設計変更をせずにサクッとテスト可能。

まとめ

「明日かどうか」や「通貨休日かどうか」を判定するコードを、現在日付の影響を受けずにテストする4つの方法を紹介しました。

  1. NodaTime.Testing
    • タイムゾーンや高度な日時計算を扱う場合に便利。
  2. SystemWrapper
    • .NET 標準クラスをラップしてモックしやすくするライブラリ。
  3. インターフェース + DI
    • 最もオーソドックスかつ汎用的な設計。フレームワークに依存しない。
  4. Func<DateTime> を利用する
    • 小規模プロジェクト向けの手軽な方法。

各プロジェクトの規模・設計方針やテスト要件に合わせて、この中から最適なアプローチを選択してください。こうした方法で日付を固定・モックすることで、**「明日判定」「通貨休日判定」**といった機能を安定してテストできます。

Discussion