メインコンテンツまでスキップ

第33章 テスト設計①:Coreの単体テストが簡単になる🧪💖

この章では「DBなし・APIなし」で、UseCase(Application層)とDomain(Domain層)をサクッと単体テストできるようになるよ〜!😆✨ ヘキサゴナルの気持ちよさって、まさにここ!💥


1) なんでヘキサだと “Coreテスト” が楽なの?🔷🧠

Core Test

ヘキサゴナルは Core(中心)を外側から守る設計だったよね🛡️ だから Core は基本こうなる👇

  • UI(Controller)知らない🙅‍♀️
  • DB(EF/SQL)知らない🙅‍♀️
  • 外部API(HttpClient)知らない🙅‍♀️
  • でも「保存したい」「時刻がほしい」みたいな やりたいこと はある✅

そこで登場するのが Port(interface) だよね🔌✨ Core は interface に向かって話すだけ。外側の実装(Adapter)は差し替え可能🔁

つまりテストでは…

✅ Adapter(DBなど)を用意しなくていい ✅ 代わりに “Fake/Stub” を差せば Core が動く

これが「単体テストが簡単」の正体だよ🧪💖


2) この章でやるテストの範囲🎯

ここでは Coreの単体テストに集中するよ😊

  • ✅ Domainのテスト(値の制約・計算・不変条件)💎
  • ✅ UseCaseのテスト(手順・分岐・保存呼び出し)🧭
  • ❌ DB接続するテスト(それは次章寄り:統合テスト)🗄️

3) 用語をゆるく整理:Fake / Stub / Mock 🍰

初心者向けに、まずこの感覚でOKだよ〜😊✨

  • Stub(スタブ):戻り値だけ返す係(固定の返答)📦
  • Fake(フェイク):軽い実装(InMemoryで保存できる等)🧠
  • Mock(モック):呼ばれた回数や引数を検査する係(道具でやること多い)🔍

この章は Fake(InMemory) が最強に相性いいよ💪😆


4) テストの基本型:AAAで書こう📐✨

テストはだいたいこの3段で書くと読みやすいよ👇

  • Arrange:準備する(Fake用意・入力作る)🧺
  • Act:実行する(UseCase呼ぶ)▶️
  • Assert:検証する(結果・保存された中身を確認)✅

5) ミニ題材(カフェ注文)で “Core単体テスト” を1本通す☕🧾🧪

ここからは「注文作成」を例にするね😊 (第15章の題材をそのまま使うイメージ!)


5-1) Core側の最小コード(Port + UseCase + Domain)🔌🧭💎

Port(Outbound Port)🗄️📝

public interface IOrderRepository
{
Task SaveAsync(Order order, CancellationToken ct);
}

“時刻”も外に逃がす(テストしやすさUP)⏰✨

実務だと DateTime.UtcNow がテストを壊しがちなので、ClockもPortにしちゃうのがコツ👍

public interface IClock
{
DateTimeOffset UtcNow { get; }
}

Domain(超ミニ)💎

public readonly record struct OrderId(Guid Value);

public sealed class Order
{
public OrderId Id { get; }
public DateTimeOffset CreatedAtUtc { get; }
public int TotalQuantity { get; }

public Order(OrderId id, DateTimeOffset createdAtUtc, int totalQuantity)
{
if (totalQuantity <= 0) throw new ArgumentOutOfRangeException(nameof(totalQuantity), "Quantity must be > 0");

Id = id;
CreatedAtUtc = createdAtUtc;
TotalQuantity = totalQuantity;
}
}

UseCase(Application層)🧭

public sealed record CreateOrderCommand(int TotalQuantity);

public sealed record CreateOrderResult(Guid OrderId, DateTimeOffset CreatedAtUtc);

public sealed class CreateOrderUseCase
{
private readonly IOrderRepository _repo;
private readonly IClock _clock;

public CreateOrderUseCase(IOrderRepository repo, IClock clock)
{
_repo = repo;
_clock = clock;
}

public async Task<CreateOrderResult> HandleAsync(CreateOrderCommand cmd, CancellationToken ct)
{
// ここで “業務ルール” を守る(例:数量は1以上)✅
if (cmd.TotalQuantity <= 0)
throw new ArgumentOutOfRangeException(nameof(cmd.TotalQuantity), "TotalQuantity must be > 0");

var now = _clock.UtcNow;
var order = new Order(new OrderId(Guid.NewGuid()), now, cmd.TotalQuantity);

await _repo.SaveAsync(order, ct);

return new CreateOrderResult(order.Id.Value, order.CreatedAtUtc);
}
}

5-2) テスト側:Fake Adapter(InMemory Repository)を作る🧪📦

テストプロジェクト内に、こんなのを置くと便利だよ😊

public sealed class FakeOrderRepository : IOrderRepository
{
public List<Order> Saved { get; } = new();

public Task SaveAsync(Order order, CancellationToken ct)
{
Saved.Add(order);
return Task.CompletedTask;
}
}

Clockも固定しよう⏰(これ大事!)

public sealed class FakeClock : IClock
{
public DateTimeOffset UtcNow { get; set; }
}

5-3) xUnitで “まず1本” 書く!🧪💖

※ xUnit は v3 も出ていて、今どきの環境では xUnit3 対応情報も出てるよ(後ろでちょい触れるね)🧠✨ (xUnit.net)

using Xunit;

public sealed class CreateOrderUseCaseTests
{
[Fact]
public async Task HandleAsync_ValidCommand_SavesOrderAndReturnsResult()
{
// Arrange 🧺
var repo = new FakeOrderRepository();
var clock = new FakeClock { UtcNow = new DateTimeOffset(2026, 1, 23, 0, 0, 0, TimeSpan.Zero) };

var sut = new CreateOrderUseCase(repo, clock);
var cmd = new CreateOrderCommand(totalQuantity: 2);

// Act ▶️
var result = await sut.HandleAsync(cmd, CancellationToken.None);

// Assert ✅
Assert.NotEqual(Guid.Empty, result.OrderId);
Assert.Equal(clock.UtcNow, result.CreatedAtUtc);

Assert.Single(repo.Saved);
Assert.Equal(2, repo.Saved[0].TotalQuantity);
Assert.Equal(clock.UtcNow, repo.Saved[0].CreatedAtUtc);
}

[Fact]
public async Task HandleAsync_InvalidQuantity_Throws()
{
// Arrange 🧺
var repo = new FakeOrderRepository();
var clock = new FakeClock { UtcNow = DateTimeOffset.UtcNow };
var sut = new CreateOrderUseCase(repo, clock);

// Act & Assert ✅
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
sut.HandleAsync(new CreateOrderCommand(0), CancellationToken.None));

// ついでに「保存されてない」も確認しちゃう😊
Assert.Empty(repo.Saved);
}
}

🎉 これで「DBなしで、注文作成の手順を検証できた!」って状態になったよ😆✨ この時点で、もうヘキサの勝ちを感じるはず…!🏆


6) Visual Studioでの回し方(Windows)🪟🧪

  • テストプロジェクトを右クリック → テストの実行 ▶️
  • テスト エクスプローラーで一覧が見える👀✨
  • 失敗したらクリックで詳細が出る🧯

あと「デバッグしたい!」なら、普通にブレークポイント置いて F5 でもOKな流れが整ってるよ(特に Microsoft.Testing.Platform の説明でも “テストプロジェクトをスタートアップにしてF5でデバッグ” が案内されてる)🧠🔍 (Microsoft Learn)


7) いまどきの .NET テスト周りの “空気” も軽く押さえよう🌬️🧠

7-1) .NET 10 / C# 14 が現行の軸になってるよ🧩✨

  • .NET 10 は LTS として 2025-11-11 リリース(サポートも長め)🛡️ (Microsoft for Developers)
  • C# 14 は .NET 10 対応の “最新C#” として整理されてるよ🧠✨ (Microsoft Learn)
  • Visual Studio 2026 も .NET 10 を含む流れが前提になってる📦 (Microsoft Learn)

7-2) “Microsoft.Testing.Platform” っていう新しい流れもある🧪🚀

最近の .NET はテスト実行基盤が進化中で、Microsoft.Testing.Platform への移行ガイドも公式で用意されてるよ📘✨ (Microsoft Learn)

設定ファイルも [appname].testconfig.json って形で案内されてる(必要になった時に思い出せばOK)🧠 (Microsoft Learn)

MSTest を Microsoft.Testing.Platform で動かす場合は、プロジェクトに <EnableMSTestRunner>true</EnableMSTestRunner><OutputType>Exe</OutputType> を足す、みたいな “オプトイン” が紹介されてるよ🔧 (Microsoft Learn)

👉 でもこの章の結論はシンプル: Coreの単体テストは「Fake差して動かす」さえできれば勝ち😆💖


8) “Core単体テスト” を強くするコツ集💪✨

コツA:外部要素はPortにして固定できるようにする🔌🧊

  • 時刻(Clock)⏰
  • ID生成(必要なら)🆔
  • 乱数🎲

「テストが不安定」って悩みの8割はここで消えるよ😊✨

コツB:テストは速く・少なく・気持ちよく🏎️💨

  • 1テストは 10〜50ms くらいで走るのが理想(体感)⚡
  • 速いと「常に回す」文化になって勝てる🏆

コツC:Mockライブラリは “必要になってから” でもOK🙂

最初は Fake だけで十分! 「呼ばれた回数を厳密に見たい」みたいな局面で Mock を導入すればいいよ🔍✨


9) AI(Copilot/Codex等)をテストで使うコツ🤖🧪✨

AIに任せやすいのはここ👇

  • Arrange の雛形(Fake作成、入力作成)🧺
  • Assert の候補(何を検証するべき?)✅
  • テストケース洗い出し(境界値)📌

でも、人間が必ず握るのはここ👇🚦

  • 「Coreのテストは外部に触れない」ルール🛡️
  • 何が業務ルールで、何が技術都合かの切り分け🧠

AIに投げるプロンプト例(そのまま使ってOK)📝✨

  • 「CreateOrderUseCase の正常系/異常系の単体テストを xUnit で AAA 形式で作って。Repository と Clock は Fake で。」🤖
  • 「TotalQuantity の境界値(0, 1, 大きい値)でテストケース提案して」📌
  • 「Domain の不変条件に対するテストを3本提案して」💎

10) ミニ演習(3本だけ!)🏋️‍♀️🧪

  1. TotalQuantity=1 のとき成功する✅
  2. TotalQuantity=-1 のとき例外になる🧯
  3. HandleAsync が呼ばれたら repo.Saved が増える📦

たったこれだけで「Coreテストの型」が身体に入るよ😆✨


11) この章のチェックリスト✅📝

  • UseCase を Fake Adapter で動かせた?🔌
  • DBなしでテストが回った?🗄️🚫
  • 時刻などの外部要素を Portに逃がした?⏰
  • AAAで読みやすく書けた?📐
  • 失敗時に “何が壊れたか” すぐ分かる?🧯

次の第34章では「Adapter側のテスト(DBや外部APIを含む)」が 別ゲーム だよ〜!🔍🧪 Coreが固まってるからこそ、外側は安心して “別枠” で育てられるんだ😊✨