本文へスキップ
BecomeCoder

C# MVVMコース · 第3章 Factory ― 生成をひとまとめに · レッスン11

Helper ― 時刻・ID生成を切り出す

ブラウザで完結

導入

TaskFactoryGuid.NewGuid()を直接呼んでいます。これは「実行するたびに値が変わる」という点でDateTime.Nowと同じ性質を持ちます。今はまだ困りませんが、この先の章で「時刻に依存する機能(期限切れの判定など)」をテストしたくなったとき、本物のDateTime.Nowのままでは「今この瞬間」に依存してテストが書けません。こういう「実行するたびに変わる値」を、IClockIIdGeneratorという小さな抽象の後ろに隠しておくと、テストでは固定値を返す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を見る

本番用のGuidIdGeneratorGuid.NewGuid()を返す)も同じ形で作れますが、このテストではFakeIdGeneratorだけで十分です

まとめ

  • DateTime.NowGuid.NewGuid()のように「実行するたびに変わる値」を直接コードに埋め込むと、テストが非決定的になる
  • IClock/IIdGeneratorのような薄い抽象を挟み、本番は本物(SystemClock/GuidIdGenerator)・テストはFakeを注入すると、同じロジックを決定的にテストできる
  • 第10章で「期限切れの強調表示」を作るとき、このIClockの考え方をそのまま使う

次章: 作ったタスクを保持しておくRepositoryです。

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。