🐕

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

に公開

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

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


1. NodaTime.Testing を使う場合

HolidayChecker 側(休日として扱う例)

using NodaTime;
using NodaTime.Testing;

public class HolidayChecker
{
    // ※名称を Holidays に変更(「休日」リスト)
    private static readonly DateTime[] Holidays = 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 IsHoliday(DateTime targetDate)
    {
        return Holidays.Any(h => h.Date == targetDate.Date);
    }
}

テストコード例

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_NodaTimeFakeClock()
    {
        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-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 IsHoliday_NodaTimeFakeClock()
    {
        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);

        // Holidays 配列に含まれる2025-01-01は「休日」として判定される
        Assert.IsTrue(checker.IsHoliday(new DateTime(2025, 1, 1)));
        Assert.IsFalse(checker.IsHoliday(new DateTime(2025, 1, 8)));
    }
}

2. SystemWrapper を使う場合

HolidayChecker 側(SystemWrapper 版)

using SystemWrapper.Interfaces;
using SystemWrapper;

public class HolidayChecker
{
    // 「休日」リストとして定義
    private static readonly DateTime[] Holidays = new[]
    {
        new DateTime(2025, 1, 1),
        new DateTime(2025, 2, 11),
        new DateTime(2025, 12, 25)
    };

    private readonly IDateTime _dateTime;

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

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

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

テストコード例

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_SystemWrapper()
    {
        var mockDateTime = new Mock<IDateTime>();
        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 IsHoliday_SystemWrapper()
    {
        var mockDateTime = new Mock<IDateTime>();
        mockDateTime.Setup(dt => dt.Now).Returns(new DateTime(2025, 1, 8));

        var checker = new HolidayChecker(mockDateTime.Object);

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

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

インターフェース定義

public interface IClock
{
    DateTime Now { get; }
}

本番用実装

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

HolidayChecker 側(休日として扱う例)

public class HolidayChecker
{
    // 「休日」リストに名称変更
    private static readonly DateTime[] Holidays = new[]
    {
        new DateTime(2025, 1, 1),
        new DateTime(2025, 2, 11),
        new DateTime(2025, 12, 25)
    };

    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 IsHoliday(DateTime targetDate)
    {
        return Holidays.Any(h => h.Date == targetDate.Date);
    }
}

テストコード例

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_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.IsTomorrow(new DateTime(2025, 1, 9)));
        Assert.IsFalse(checker.IsTomorrow(new DateTime(2025, 1, 8)));
    }

    [Test]
    public void IsHoliday_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.IsHoliday(new DateTime(2025, 1, 1)));
        Assert.IsFalse(checker.IsHoliday(new DateTime(2025, 1, 10)));
    }
}

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

HolidayChecker 側(最初の例と同様)

public class HolidayChecker
{
    // 「休日」リストとして定義
    private static readonly DateTime[] Holidays = 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;
    }

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

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

テストコード例

[TestFixture]
public class HolidayCheckerTests
{
    [Test]
    public void IsTomorrow_Func()
    {
        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 IsHoliday_Func()
    {
        Func<DateTime> fixedNow = () => new DateTime(2025, 1, 8);
        var checker = new HolidayChecker(fixedNow);

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

まとめ

  • 明日判定のテストで日付が動的になっても再現性のあるテストが実現でき、
  • 休日判定のテストで、実際の休日リストに依存せずにテストできる
    といったメリットが得られます。

このように、実装パターンごとに「現在日付を自由に制御」する方法を採用することで、日時に左右されないテストの実現と、ドメインにおける休日判定の明確化を行えます。

Discussion