導入
「新しいIDを発行する」「今の日時を取る」――こうした小さな共通処理をヘルパークラスに切り出すことはよくあります。ただし、これを静的(static)なメソッドにするか、インターフェースにしてインスタンスをDIで注入するかは、テストのしやすさに直結する設計判断です。
説明
flowchart LR
subgraph static_["static ヘルパー"]
S["GuidIdGenerator.NewId()"] -->|直接呼ぶ| X["呼ぶたびに違う値<br/>テストで固定できない"]
end
subgraph di["interface + DI"]
I["IIdGenerator(抽象)"] -.-> Real["本番: GuidIdGenerator"]
I -.-> Fake["テスト: SequentialIdGenerator<br/>(予測できる値を返す)"]
end
style Fake fill:#e8f5e9
判断の目安はシンプルです。
- 純粋な計算(入力だけで結果が決まり、副作用がない):
staticのままで十分テストできる。文字列整形や数値計算のようなヘルパーがこれにあたる - 呼ぶたびに結果が変わる/外部の状態に触れる(現在時刻・乱数・新規ID発行・ファイルI/O):
staticのまま直接呼ぶと、テストで結果を固定できない。インターフェースに抽象化してコンストラクタで注入し、テストでは予測可能な値を返すFakeに差し替える
具体例で確認しましょう。Guid.NewGuid()のような「毎回違う値」を返す処理そのものはテストしにくいものの、それをIIdGeneratorという抽象の後ろに隠すことで、それを使う側のクラスはテストしやすくなります。
interface IIdGenerator { string NewId(); }
// 本番用: 呼ぶたびに違う値になる(そのままでは戻り値をテストで固定できない)
class GuidIdGenerator : IIdGenerator
{
public string NewId() => Guid.NewGuid().ToString();
}
class TicketCounter
{
private readonly IIdGenerator _idGenerator;
public TicketCounter(IIdGenerator idGenerator) => _idGenerator = idGenerator; // 注入
public string IssueTicket() => _idGenerator.NewId();
}
TicketCounter自体はIIdGeneratorという抽象にしか依存していない- テストでは
SequentialIdGenerator("id-1","id-2", … のように予測できる値を返すFake)を注入すれば、発行されたIDをそのままAssertできる
やってみよう
SequentialIdGeneratorを実装し、TicketCounterが発行するIDを実際に予測できることを確認してみましょう。
演習
var fakeIds = new SequentialIdGenerator();
var counter = new TicketCounter(fakeIds);
Assert(counter.IssueTicket() == "id-1", "1件目はid-1");
Assert(counter.IssueTicket() == "id-2", "2件目はid-2");
Assert(counter.Issued.Count == 2, "発行履歴が2件残る");
Console.WriteLine("全テスト成功");
interface IIdGenerator { string NewId(); }
// 本番用: 呼ぶたびに違う値になるので、そのままではテストで固定できない
class GuidIdGenerator : IIdGenerator
{
public string NewId() => Guid.NewGuid().ToString();
}
// TODO: 呼ばれた回数から "id-1", "id-2", ... の順に返す
// SequentialIdGenerator を実装してください
class SequentialIdGenerator : IIdGenerator
{
___
}
class TicketCounter
{
private readonly IIdGenerator _idGenerator;
private readonly List<string> _issued = new();
public TicketCounter(IIdGenerator idGenerator) => _idGenerator = idGenerator;
public string IssueTicket()
{
var id = _idGenerator.NewId();
_issued.Add(id);
return id;
}
public IReadOnlyList<string> Issued => _issued;
}
void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }
- 期待される出力:
全テスト成功
ヒント1を見る
class SequentialIdGenerator : IIdGenerator { private int _count = 0; public string NewId() => $"id-{++_count}"; }
ヒント2を見る
フィールドでカウンタを1件ずつ増やしながら"id-" + カウンタを文字列補間で返します
まとめ
- 副作用のない純粋なヘルパーは
staticのままで十分テストできる - 現在時刻・乱数・新規ID発行のような「呼ぶたびに変わる」ヘルパーはインターフェースに抽象化し、コンストラクタで注入する
- 本番は
GuidIdGenerator、テストはSequentialIdGeneratorのような予測可能なFakeに差し替えられる TaskFactory.Createが内部でGuid.NewGuid()を直接呼んでいるのは、Idの実際の値をテストで検証する必要がない(重複さえしなければよい)からこそ許される設計判断
次回: MVVMでやってしまいがちな失敗、アンチパターンです。