導入
アプリが育つと、DB(または本物のファイル保存)を使うInMemoryTaskRepositoryだけではテストがつらくなります。DBの起動待ち・後片付け・実行順で結果が変わる不安定さ……。そこでテスト専用の偽物(Fake)を用意します。
図解
flowchart LR
T["TaskService(テスト対象)"] --> I["ITaskRepository(抽象)"]
I -->|本番| R["InMemoryTaskRepository<br/>(実際に保持)"]
I -->|テスト| F["FakeTaskRepository<br/>(挙動を固定・呼び出しを記録)"]
style F fill:#e8f5e9
サンプル
考え方はC#デザインパターンコースのMoq(手動フェイク)と同じです。Moqがやっていることを、このITaskRepositoryに対して手で書きます。
// 手動フェイク: 本物のように振る舞うが、中身は単純なListだけ
public class FakeTaskRepository : ITaskRepository
{
private readonly List<TaskItem> _tasks = new();
public int AddCallCount { get; private set; } // 「何回呼ばれたか」を記録できるのがFakeの強み
public void Add(TaskItem task) { _tasks.Add(task); AddCallCount++; }
public IReadOnlyList<TaskItem> All() => _tasks;
public TaskItem? FindById(Guid id) => _tasks.FirstOrDefault(t => t.Id == id);
}
- 速い: メモリ上のListだけなので、DB接続やファイルI/Oを待たない
- 決定的: 毎回同じ結果になる(DBの残留データに左右されない)
- DBが無くても書ける: サーバー担当が実装を書く前でも、Fakeさえあればテストを書き始められる
- おまけに
AddCallCountのように「呼ばれた回数」まで記録できる(MoqのVerifyに相当)
やってみよう
FakeTaskRepositoryにFindByIdが呼ばれた回数を数えるFindByIdCallCountも足して、何回検索されたか確認してみましょう。
演習
var fake = new FakeTaskRepository();
var t1 = TaskFactory.Create("牛乳を買う");
fake.Add(t1);
fake.Add(TaskFactory.Create("卵を買う"));
Assert(fake.AddCallCount == 2, "Addが2回呼ばれた");
Assert(fake.All().Count == 2, "2件保持している");
Assert(fake.FindById(t1.Id)?.Title.Value == "牛乳を買う", "IDで取得できる");
Console.WriteLine("全テスト成功");
// TODO: ITaskRepository を実装し、Add が呼ばれた回数を AddCallCount で数える
// FakeTaskRepository を実装してください
___
void Assert(bool c, string n) { if (!c) throw new Exception($"FAIL: {n}"); }
// --- 型のコピー ---
public readonly struct TaskTitle : IEquatable<TaskTitle>
{
public string Value { get; }
public TaskTitle(string value)
{
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("タイトルは空にできません");
if (value.Length > 50) throw new ArgumentException("タイトルは50文字以内です");
Value = value.Trim();
}
public bool Equals(TaskTitle other) => Value == other.Value;
public override string ToString() => Value;
}
public enum Priority { Low, Medium, High }
public class TaskItem
{
public Guid Id { get; }
public TaskTitle Title { get; private set; }
public Priority Priority { get; private set; }
public bool IsCompleted { get; private set; }
public TaskItem(Guid id, TaskTitle title, Priority priority)
{ Id = id; Title = title; Priority = priority; }
public void Complete() => IsCompleted = true;
public void Rename(TaskTitle newTitle) => Title = newTitle;
}
public static class TaskFactory
{
public static TaskItem Create(string title, Priority priority = Priority.Medium)
=> new TaskItem(Guid.NewGuid(), new TaskTitle(title), priority);
}
public interface ITaskRepository
{
void Add(TaskItem task);
IReadOnlyList<TaskItem> All();
TaskItem? FindById(Guid id);
}
- 期待される出力:
全テスト成功
ヒント1を見る
public class FakeTaskRepository : ITaskRepository { private readonly List<TaskItem> _tasks = new(); public int AddCallCount { get; private set; } public void Add(TaskItem task) { _tasks.Add(task); AddCallCount++; } public IReadOnlyList<TaskItem> All() => _tasks; public TaskItem? FindById(Guid id) => _tasks.FirstOrDefault(t => t.Id == id); }
ヒント2を見る
Addの中で_tasks.Add(task)のあとにAddCallCount++を忘れずに
まとめ
- Fakeは「速い・決定的・DB不要」でテストを書けるようにする手動の偽物
ITaskRepositoryを実装する点は本物と同じなので、テスト対象からは区別がつかない- 呼び出し回数のような「検証」用の情報も自由に足せる
次回: TaskServiceにこのITaskRepositoryをコンストラクタで注入します。