C#で日付をテストする際のTips (明日かどうかと通貨休日かどうかを判定する)
「明日かどうか」「通貨休日かどうか」を判定する機能をテストするときに、現在日付を自由に制御(固定)してテストするための実装パターンを解説します。テストをする際に「日付や時刻が動的に変わってしまうため再現性が保てない」「明日かどうかの判定が日付によって失敗する」「休日が正しく判定できるか日付に左右される」といった問題に対処する上で役立ちます。
ここでは例として、以下のような判定メソッドを想定します。
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.Now
や DateTime.UtcNow
を直接使ってしまうと、「今日」の日付が変わればテスト結果も変わってしまいます。そこで、現在日付をモック(仮想)にするための4つの実装パターンをご紹介します。
1. NodaTime.Testing を使う方法
概要
NodaTime は高機能な日時ライブラリで、テスト用に「現在時刻を固定」できる FakeClock
クラスを提供しています。タイムゾーンの扱いが多いプロジェクトでは特に有効です。
実装例
1.1 HolidayChecker 側の対応
NodaTime を使う場合、DateTime.Now
をそのまま呼び出すのではなく、NodaTime で取得した日時オブジェクト(Instant
や ZonedDateTime
)を使う設計にします。
しかしここでは、簡易的に「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つの方法を紹介しました。
-
NodaTime.Testing
- タイムゾーンや高度な日時計算を扱う場合に便利。
-
SystemWrapper
- .NET 標準クラスをラップしてモックしやすくするライブラリ。
-
インターフェース + DI
- 最もオーソドックスかつ汎用的な設計。フレームワークに依存しない。
-
Func<DateTime> を利用する
- 小規模プロジェクト向けの手軽な方法。
各プロジェクトの規模・設計方針やテスト要件に合わせて、この中から最適なアプローチを選択してください。こうした方法で日付を固定・モックすることで、**「明日判定」「通貨休日判定」**といった機能を安定してテストできます。
Discussion