導入
TaskFactoryはGuid.NewGuid()を直接呼んでいます。これは「実行するたびに値が変わる」という点でDateTime.Nowと同じ性質を持ちます。今はまだ困りませんが、この先の章で「時刻に依存する機能(期限切れの判定など)」をテストしたくなったとき、本物のDateTime.Nowのままでは「今この瞬間」に依存してテストが書けません。こういう「実行するたびに変わる値」を、IClock・IIdGeneratorという小さな抽象の後ろに隠しておくと、テストでは固定値を返すFakeに差し替えられます。
図解
classDiagram
class IClock {
<<interface>>
+Now() DateTime
}
class SystemClock
class FakeClock
IClock <|.. SystemClock
IClock <|.. FakeClock
class IIdGenerator {
<<interface>>
+NewId() Guid
}
class GuidIdGenerator
class FakeIdGenerator
IIdGenerator <|.. GuidIdGenerator
IIdGenerator <|.. FakeIdGenerator
サンプル
// DateTime.Now に直接依存すると、実行するたびに結果が変わってテストしにくい
bool IsMorning() => DateTime.Now.Hour < 12; // 今実行したら何が返る? 実行時刻次第
// IClockという薄い抽象を挟むと、Fakeで時刻を固定してテストできる
interface IClock { DateTime Now(); }
class SystemClock : IClock { public DateTime Now() => DateTime.Now; } // 本番用:本物の時刻
class FakeClock : IClock // テスト用:固定した時刻
{
private readonly DateTime _now;
public FakeClock(DateTime now) => _now = now;
public DateTime Now() => _now;
}
bool IsMorningAt(IClock clock) => clock.Now().Hour < 12;
Assert(IsMorningAt(new FakeClock(new DateTime(2026, 1, 1, 8, 0, 0))) == true, "8時は午前");
Assert(IsMorningAt(new FakeClock(new DateTime(2026, 1, 1, 20, 0, 0))) == false, "20時は午前ではない");
Console.WriteLine("全テスト成功");
void Assert(bool condition, string name)
{
if (!condition) throw new Exception($"FAIL: {name}");
}
DateTime.Nowを直接呼ぶコードは「いつ実行しても同じ結果」を保証できず、テストしにくい(非決定的)IClockという抽象を挟むと、本番はSystemClock(本物の時刻)、テストはFakeClock(固定した時刻)を渡せるGuid.NewGuid()も「実行するたびに変わる値」という点で同じ。同じ考え方でIIdGeneratorに抽象化できる
演習
IIdGeneratorと、時刻・IDを組み合わせた文字列を作るStamperをTDDで実装します。
var stamper = new Stamper(
new FakeClock(new DateTime(2026, 1, 1)),
new FakeIdGenerator(Guid.Parse("11111111-1111-1111-1111-111111111111")));
Assert(stamper.Stamp() == "11111111-1111-1111-1111-111111111111@2026-01-01",
"固定のIDと時刻を注入すれば、何度実行しても同じ文字列になる");
Console.WriteLine("全テスト成功");
interface IClock { DateTime Now(); }
class FakeClock : IClock
{
private readonly DateTime _now;
public FakeClock(DateTime now) => _now = now;
public DateTime Now() => _now;
}
// TODO: 次を満たす IIdGenerator・FakeIdGenerator・Stamper を実装してください
// ・interface IIdGenerator は Guid NewId() を持つ
// ・FakeIdGenerator(Guid id) は常に同じ id を NewId() で返す
// ・Stamper(IClock clock, IIdGenerator idGenerator) を受け取り、
// Stamp() で $"{idGenerator.NewId()}@{clock.Now():yyyy-MM-dd}" を返す
void Assert(bool condition, string name)
{
if (!condition) throw new Exception($"FAIL: {name}");
}
___
- 期待される出力:
全テスト成功
ヒント1を見る
interface IIdGenerator { Guid NewId(); } class FakeIdGenerator : IIdGenerator { private readonly Guid _id; public FakeIdGenerator(Guid id) => _id = id; public Guid NewId() => _id; } class Stamper { private readonly IClock _clock; private readonly IIdGenerator _idGenerator; public Stamper(IClock clock, IIdGenerator idGenerator) { _clock = clock; _idGenerator = idGenerator; } public string Stamp() => $"{_idGenerator.NewId()}@{_clock.Now():yyyy-MM-dd}"; }
ヒント2を見る
本番用のGuidIdGenerator(Guid.NewGuid()を返す)も同じ形で作れますが、このテストではFakeIdGeneratorだけで十分です
まとめ
DateTime.NowやGuid.NewGuid()のように「実行するたびに変わる値」を直接コードに埋め込むと、テストが非決定的になるIClock/IIdGeneratorのような薄い抽象を挟み、本番は本物(SystemClock/GuidIdGenerator)・テストはFakeを注入すると、同じロジックを決定的にテストできる- 第10章で「期限切れの強調表示」を作るとき、この
IClockの考え方をそのまま使う
次章: 作ったタスクを保持しておくRepositoryです。