第44章:UseCaseのテスト(Port差し替えで外部なし)🎭🧪✨
📌 2026年1月時点の“最新テスト事情”だけ先にサクッと🥰
- いまの最新は **.NET 10(LTS)**で、2026/01/13 時点の最新リリースが 10.0.2 になってるよ📦✨ (Microsoft)
- Visual Studio 2026 は 2026/01/20 に 18.2.1 が出てるよ🧰✨ (Microsoft Learn)
dotnet testは **VSTest(既定)**と、.NET 10 SDK で入った Microsoft Testing Platform(MTP)モードの“2モード”が公式に整理されたよ🧪🔁 (Microsoft Learn) (でも!この章では、まずは **いつものテンプレのまま(VSTest既定)**で全然OK👌 迷子にならないのが大事✨)
この章でできるようになること🎯💖
- DBなし / HTTPなしで UseCase(Interactor)をテストできる🥳
- Repository / Presenter を Fake に差し替えて、UseCaseの手順だけ検証できる🔌🎭
- 「落ちるテスト」じゃなくて「仕様が読めるテスト」が書ける📖✨
1) UseCaseテストってどこを狙うの?🏹🎯

UseCaseのテストは、気持ちとしては **“ほぼ単体テスト寄り”**だよ🫶 ポイントはこれ👇
- 単体テストは「自分のコントロール下のコードだけをテストして、DB/ネットワークみたいなインフラ問題は含めない」って公式にも書かれてるのね✨ (Microsoft Learn)
- クリーンアーキのUseCaseは、もともと 外部(DB/HTTP)を Port(interface)越しにするから、差し替えが超やりやすい😳💡
つまり… Port差し替え = 外部を切っても UseCase の仕様は検証できる! 🎉
2) 今日の主役:Fake / Stub / Spy を“ゆるく”覚える😆🧸
この章では難しい言葉をガチ暗記しなくてOK🙆♀️💕 ざっくりこう使うよ👇
- Fake:それっぽく動くニセ実装(インメモリRepositoryとか)🧪📦
- Spy:呼ばれたか・何が渡ったかを記録する子(Presenterでよく使う)🕵️♀️📝
- Stub:決まった値を返すだけ(Getで固定のMemo返すとか)🧷
3) テストプロジェクトの作り方(迷子ゼロ版)🧰✨
✅ 依存のルール(ここ超だいじ💗)
テストプロジェクトは基本こう👇
- 参照していい:Entities / UseCases(Core側) ✅
- 参照しない:Web(ASP.NET) / EF Core / 外部API Adapter ❌
これだけで「UseCaseテストが重くなる病」だいぶ防げるよ🥹✨
✅ 実行方法(2つ)
- Visual Studio:テスト エクスプローラーで実行▶️🧪
- CLI:
dotnet testで実行🖥️🧪 ※dotnet testは VSTestが既定で、.NET 10からMTPモードもあるよ〜って整理が公式にあるよ📚 (Microsoft Learn) (最初は既定のままでOK👌)
4) 例題:CreateMemo UseCase を “外部なし”で叩く🎮🧪
ここから **「Fake Repository」+「Spy Presenter」**でやるよ🎭✨ (最小構成のサンプルだから、自分の実コードに合わせて読み替えてね🫶)
🧩 Core側(UseCaseとPort)イメージ
// Port(出口): 保存
public interface IMemoRepository
{
Task AddAsync(Memo memo, CancellationToken ct);
Task<Memo?> FindByIdAsync(Guid id, CancellationToken ct);
Task UpdateAsync(Memo memo, CancellationToken ct);
}
// Port(出口): 出力
public interface ICreateMemoOutputPort
{
void Present(CreateMemoResponse response);
}
// 入力モデル
public sealed record CreateMemoRequest(string Title);
// 出力モデル(成功/失敗を“結果”で返す派)
public sealed record CreateMemoResponse(
bool IsSuccess,
Guid? MemoId,
string? Title,
string? ErrorCode,
string? ErrorMessage
)
{
public static CreateMemoResponse Success(Guid id, string title)
=> new(true, id, title, null, null);
public static CreateMemoResponse Failure(string code, string message)
=> new(false, null, null, code, message);
}
// 例:Entity(最小)
public sealed class Memo
{
public Guid Id { get; }
public string Title { get; private set; }
public Memo(Guid id, string title)
{
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("Title is required.", nameof(title));
if (title.Length > 100)
throw new ArgumentException("Title is too long.", nameof(title));
Id = id;
Title = title;
}
public void Rename(string newTitle)
{
if (string.IsNullOrWhiteSpace(newTitle))
throw new ArgumentException("Title is required.", nameof(newTitle));
if (newTitle.Length > 100)
throw new ArgumentException("Title is too long.", nameof(newTitle));
Title = newTitle;
}
}
// ちょい工夫:ID生成も差し替えるとテストが安定💖
public interface IIdGenerator
{
Guid NewGuid();
}
public sealed class SystemIdGenerator : IIdGenerator
{
public Guid NewGuid() => Guid.NewGuid();
}
// UseCase(Interactor)
public sealed class CreateMemoInteractor
{
private readonly IMemoRepository _repo;
private readonly IIdGenerator _ids;
private readonly ICreateMemoOutputPort _out;
public CreateMemoInteractor(IMemoRepository repo, IIdGenerator ids, ICreateMemoOutputPort output)
{
_repo = repo;
_ids = ids;
_out = output;
}
public async Task HandleAsync(CreateMemoRequest request, CancellationToken ct)
{
try
{
var id = _ids.NewGuid();
var memo = new Memo(id, request.Title);
await _repo.AddAsync(memo, ct);
_out.Present(CreateMemoResponse.Success(memo.Id, memo.Title));
}
catch (ArgumentException ex)
{
// ここは例:ドメイン/入力エラーを結果で返す
_out.Present(CreateMemoResponse.Failure("ValidationError", ex.Message));
}
}
}
5) テスト側:Fake Repository と Spy Presenter を用意🎭🕵️♀️
✅ Fake Repository(インメモリ)
public sealed class FakeMemoRepository : IMemoRepository
{
private readonly Dictionary<Guid, Memo> _store = new();
public int AddCallCount { get; private set; }
public int UpdateCallCount { get; private set; }
public Task AddAsync(Memo memo, CancellationToken ct)
{
AddCallCount++;
_store.Add(memo.Id, memo);
return Task.CompletedTask;
}
public Task<Memo?> FindByIdAsync(Guid id, CancellationToken ct)
{
_store.TryGetValue(id, out var memo);
return Task.FromResult(memo);
}
public Task UpdateAsync(Memo memo, CancellationToken ct)
{
UpdateCallCount++;
_store[memo.Id] = memo;
return Task.CompletedTask;
}
}
✅ Spy Presenter(呼ばれた内容を覚える)
public sealed class SpyCreateMemoPresenter : ICreateMemoOutputPort
{
public int PresentCallCount { get; private set; }
public CreateMemoResponse? LastResponse { get; private set; }
public void Present(CreateMemoResponse response)
{
PresentCallCount++;
LastResponse = response;
}
}
✅ Fixed ID Generator(テストを確定させる✨)
public sealed class FixedIdGenerator : IIdGenerator
{
private readonly Guid _fixed;
public FixedIdGenerator(Guid fixedId) => _fixed = fixedId;
public Guid NewGuid() => _fixed;
}
6) Given-When-Then でテストを書く(超読みやすい💖)🧁✨
✅ 成功ケース:タイトルOK → 保存されて、成功が出力される
using Xunit;
public sealed class CreateMemoInteractorTests
{
[Fact]
public async Task Given_ValidTitle_When_HandleAsync_Then_SavesAndPresentsSuccess()
{
// Given 🎁
var repo = new FakeMemoRepository();
var presenter = new SpyCreateMemoPresenter();
var fixedId = Guid.Parse("11111111-1111-1111-1111-111111111111");
var ids = new FixedIdGenerator(fixedId);
var sut = new CreateMemoInteractor(repo, ids, presenter);
// When ⚡
await sut.HandleAsync(new CreateMemoRequest("Hello Memo"), CancellationToken.None);
// Then ✅
Assert.Equal(1, repo.AddCallCount);
Assert.Equal(1, presenter.PresentCallCount);
Assert.NotNull(presenter.LastResponse);
Assert.True(presenter.LastResponse!.IsSuccess);
Assert.Equal(fixedId, presenter.LastResponse.MemoId);
Assert.Equal("Hello Memo", presenter.LastResponse.Title);
}
}
✅ 失敗ケース:タイトル空 → 保存されない&失敗が出力される
using Xunit;
public sealed partial class CreateMemoInteractorTests
{
[Fact]
public async Task Given_EmptyTitle_When_HandleAsync_Then_DoesNotSaveAndPresentsFailure()
{
// Given 🎁
var repo = new FakeMemoRepository();
var presenter = new SpyCreateMemoPresenter();
var ids = new FixedIdGenerator(Guid.NewGuid());
var sut = new CreateMemoInteractor(repo, ids, presenter);
// When ⚡
await sut.HandleAsync(new CreateMemoRequest(" "), CancellationToken.None);
// Then ✅
Assert.Equal(0, repo.AddCallCount); // 保存されない
Assert.Equal(1, presenter.PresentCallCount); // 失敗が通知される
Assert.NotNull(presenter.LastResponse);
Assert.False(presenter.LastResponse!.IsSuccess);
Assert.Equal("ValidationError", presenter.LastResponse.ErrorCode);
}
}
ね?💖 DBもHTTPも一切いらないのに、UseCaseの仕様がちゃんと確認できるでしょ🥳✨
7) UpdateMemo も同じノリでいけるよ✍️🧪(ミニ版)
「既存メモを取得 → Rename → Update → 成功出力」って流れをテストするだけ🎯
🧩 UseCase(最小サンプル)
public interface IUpdateMemoOutputPort
{
void Present(UpdateMemoResponse response);
}
public sealed record UpdateMemoRequest(Guid MemoId, string NewTitle);
public sealed record UpdateMemoResponse(
bool IsSuccess,
string? ErrorCode,
string? ErrorMessage
)
{
public static UpdateMemoResponse Success() => new(true, null, null);
public static UpdateMemoResponse Failure(string code, string message) => new(false, code, message);
}
public sealed class UpdateMemoInteractor
{
private readonly IMemoRepository _repo;
private readonly IUpdateMemoOutputPort _out;
public UpdateMemoInteractor(IMemoRepository repo, IUpdateMemoOutputPort output)
{
_repo = repo;
_out = output;
}
public async Task HandleAsync(UpdateMemoRequest request, CancellationToken ct)
{
var memo = await _repo.FindByIdAsync(request.MemoId, ct);
if (memo is null)
{
_out.Present(UpdateMemoResponse.Failure("NotFound", "Memo not found."));
return;
}
try
{
memo.Rename(request.NewTitle);
await _repo.UpdateAsync(memo, ct);
_out.Present(UpdateMemoResponse.Success());
}
catch (ArgumentException ex)
{
_out.Present(UpdateMemoResponse.Failure("ValidationError", ex.Message));
}
}
}
🧪 テスト(成功)
using Xunit;
public sealed class SpyUpdateMemoPresenter : IUpdateMemoOutputPort
{
public UpdateMemoResponse? Last { get; private set; }
public void Present(UpdateMemoResponse response) => Last = response;
}
public sealed class UpdateMemoInteractorTests
{
[Fact]
public async Task Given_ExistingMemo_When_Rename_Then_UpdatesAndSuccess()
{
// Given 🎁
var repo = new FakeMemoRepository();
var presenter = new SpyUpdateMemoPresenter();
var sut = new UpdateMemoInteractor(repo, presenter);
var id = Guid.Parse("22222222-2222-2222-2222-222222222222");
await repo.AddAsync(new Memo(id, "Before"), CancellationToken.None);
// When ⚡
await sut.HandleAsync(new UpdateMemoRequest(id, "After"), CancellationToken.None);
// Then ✅
Assert.NotNull(presenter.Last);
Assert.True(presenter.Last!.IsSuccess);
Assert.Equal(1, repo.UpdateCallCount);
}
}
8) “良いUseCaseテスト”のチェックリスト✅💖
- DB/HTTP/ファイル/ネットワークがゼロ🧼
- テストが速い(何十個でも一瞬)⚡
- テスト名が仕様文(Given-When-Thenで読める)📖
- 結果はPresenter/Responseで確認(ログやConsoleに頼らない)🧠
- ランダム要素(Guid/時刻)は差し替え🧷(今回の
IIdGeneratorみたいに)
9) Copilot / Codex を使うなら(この章の“おいしい使い方”🤖🍰)
使いどころはここが最強💪✨
- Fake/Spy の雛形を一瞬で作らせる
- Given-When-Then のテストケースを列挙させる
- 境界値(0文字 / 1文字 / 100文字 / 101文字)を出させる
プロンプト例👇(そのまま投げてOK)
CreateMemoInteractor のテストを書きたいです。
DB/HTTPなしで、FakeMemoRepository と SpyPresenter を使う前提で
Given-When-Then形式のテストケースを10個、境界値多めで提案して。
まとめ🎀✨
第44章のコツはひとつだけ😊💖 UseCaseは Port(interface)越しに外部と話す → だから Fake に差し替えてテストできる 🎭🔌🧪
次の第45章で、この「依存ルール」を 自動で破れないようにする(アーキテクチャテスト)に進むよ〜!🔒✅✨