導入
前回のテストはInMemoryTaskRepositoryを使いましたが、実務でRepositoryの実体がDB接続を必要とする場合、テストのたびにDBを用意するのは大変です。レッスン14のFakeTaskRepositoryをTaskServiceに注入し、DBなしで業務ロジックだけを検証しましょう。
図解
sequenceDiagram
participant Test
participant TaskService
participant FakeTaskRepository
Test->>TaskService: new TaskService(fake)
Test->>TaskService: AddTask("牛乳を買う", ...)
TaskService->>FakeTaskRepository: Add(task)
Test->>FakeTaskRepository: AddCallCount を確認
サンプル
TaskServiceのコードは1行も変えず、渡すRepositoryだけを差し替えます。
var fake = new FakeTaskRepository();
var service = new TaskService(fake);
service.AddTask("牛乳を買う", Priority.Medium);
Console.WriteLine(fake.AddCallCount); // 1(本当にRepositoryへ渡されたか検証できる)
Console.WriteLine(service.GetActiveTasks().Count); // 1(業務ロジックの結果も検証できる)
TaskServiceはITaskRepositoryにしか依存していないため、InMemoryTaskRepositoryとFakeTaskRepositoryのどちらを渡しても動く(DIP/レッスン15)- Fakeを使えば「結果(GetActiveTasks)」だけでなく「呼び出し(AddCallCount)」まで検証できる
- DBや外部APIに依存するRepositoryほど、Fakeでのテストの価値が大きくなる
やってみよう
AddTaskで重複タイトルを渡し、fake.AddCallCountが増えていない(=Repositoryまで到達していない)ことを確認してみましょう。
演習
var fake = new FakeTaskRepository();
var service = new TaskService(fake);
service.AddTask("牛乳を買う", Priority.Medium);
service.AddTask("卵を買う", Priority.Low);
Assert(fake.AddCallCount == 2, "Addが2回呼ばれた");
Assert(service.GetActiveTasks().Count == 2, "未完了が2件");
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);
}
public interface ITaskService
{
TaskItem AddTask(string title, Priority priority);
void CompleteTask(Guid id);
IReadOnlyList<TaskItem> GetActiveTasks();
}
public class TaskService : ITaskService
{
private readonly ITaskRepository _repository;
public TaskService(ITaskRepository repository) => _repository = repository;
public TaskItem AddTask(string title, Priority priority)
{
if (_repository.All().Any(t => t.Title.Value == title))
throw new InvalidOperationException("同じタイトルのタスクが既にあります");
var task = TaskFactory.Create(title, priority);
_repository.Add(task);
return task;
}
public void CompleteTask(Guid id) => _repository.FindById(id)?.Complete();
public IReadOnlyList<TaskItem> GetActiveTasks() => _repository.All().Where(t => !t.IsCompleted).ToList();
}
- 期待される出力:
全テスト成功
ヒント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を見る
TaskService側のコードは変更不要です。渡すRepositoryをFakeTaskRepositoryにするだけで動きます
まとめ
- テスト対象(
TaskService)のコードを変えずに、依存だけをFakeに差し替えられるのがDIの効能 - Fakeなら結果の検証(
GetActiveTasks)と呼び出しの検証(AddCallCount)の両方ができる - DB・外部APIに依存するほど、Fakeによるテストの価値は大きくなる
次回: 画面に渡すデータの形を分けるTaskDto/TaskMapperです。