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

第11章:CQSとテスト①(Queryはラク勝ち🧪🏆)

この章はね、めっちゃ気持ちいい回です🥰 なぜなら Query(参照)は副作用がない前提 なので、テストが「入力→出力」だけで完結しやすいから!✨

ちなみに今の最新環境だと、**.NET 10(LTS)**が基準で話せます👍 (Microsoft) Visual Studioも Visual Studio 2026(2026/1/13 のアップデートあり) まで来てて、テスト体験もかなり強化されてるよ〜🛠️✨ (Microsoft Learn)


1) 今日のゴール🎯✨

ゴールはこれだけ!シンプル!😊

  • Queryのテストがラクな理由を体感する🧠🌱
  • xUnit(または MSTest / NUnit)で、Queryの単体テストを書ける🧪
  • Visual Studioの Test Explorer で「実行・デバッグ」できる🔍🪲 (Microsoft Learn)
  • AI(Copilot/Codex)に テストケース案 を出させて、良いテストに育てる🤖📝 (Microsoft Learn)

2) Queryテストがラクな理由😍✨(CQSのご褒美)

Input Output Test

Queryはこういう存在👇

  • 入力:検索条件・ID・フィルタなど🔍
  • 出力:欲しいデータ(一覧、詳細、検索結果)📦
  • 状態を変えない(DB更新しない、ファイル書かない、メール送らない)

だからテストが…

だけで終わる!💯✨


3) 今日の題材:TodoQueries をテストする📝🍰

ここまでの章で作ってきた想定の “分離” はこんな感じ👇

  • TodoQueries:参照だけ担当🔍
  • TodoCommands:変更だけ担当🔧(これは第12章でテストするよ🎭)

今回は TodoQueries に集中!🏃‍♀️💨


4) まずは “テストしやすいQuery” の形にする🏗️✨

ポイントはこれ👇 Queryが依存するのは「読むための窓口」だけにする(=読み取り専用の依存)😊

例:読み取り用リポジトリ(インターフェース)📌

public sealed record TodoItem(Guid Id, string Title, bool IsCompleted, int Priority);

public interface ITodoReadRepository
{
IReadOnlyList<TodoItem> GetAll();
TodoItem? FindById(Guid id);
}

そして Query クラス👇

public sealed class TodoQueries
{
private readonly ITodoReadRepository _repo;

public TodoQueries(ITodoReadRepository repo)
{
_repo = repo;
}

// Query①:一覧(優先度が高い順 → タイトル順)
public IReadOnlyList<TodoItem> GetAll()
=> _repo.GetAll()
.OrderByDescending(x => x.Priority)
.ThenBy(x => x.Title, StringComparer.OrdinalIgnoreCase)
.ToList();

// Query②:完了だけ
public IReadOnlyList<TodoItem> GetCompleted()
=> _repo.GetAll().Where(x => x.IsCompleted).ToList();

// Query③:検索(タイトルに含む:大文字小文字を無視)
public IReadOnlyList<TodoItem> SearchByTitle(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword)) return Array.Empty<TodoItem>();

return _repo.GetAll()
.Where(x => x.Title.Contains(keyword, StringComparison.OrdinalIgnoreCase))
.ToList();
}

// Query④:IDで1件
public TodoItem? GetById(Guid id) => _repo.FindById(id);
}

ここまでくると、テストがめちゃ簡単になるよ〜🥳✨


5) テスト用の “インメモリRepo” を作る🧊✅(モック不要!)

Queryテストはまず in-memory(メモリ上のリスト)で十分!👍

public sealed class InMemoryTodoReadRepository : ITodoReadRepository
{
private readonly List<TodoItem> _items;

public InMemoryTodoReadRepository(IEnumerable<TodoItem> items)
{
_items = items.ToList();
}

public IReadOnlyList<TodoItem> GetAll() => _items;

public TodoItem? FindById(Guid id) => _items.FirstOrDefault(x => x.Id == id);
}

6) Queryテストで見る観点(ここが超大事!)🔍✨

Queryのテストは、だいたいこの4種類で勝てます🏆

  1. 境界(null/空文字/大小文字/存在しないID)🧱

7) xUnitでテストを書く🧪✨(AAAでいこう)

Microsoft公式にも xUnit/NUnit/MSTest の流れがまとまってるので、迷ったらここ基準でOKだよ😊 (Microsoft Learn) (今回は例として xUnit で!)

7-1) 並び順テスト(ソート)🔃✅

using Xunit;

public sealed class TodoQueriesTests
{
[Fact]
public void GetAll_should_sort_by_priority_desc_then_title_asc()
{
// Arrange 🧸
var a = new TodoItem(Guid.NewGuid(), "banana", false, priority: 1);
var b = new TodoItem(Guid.NewGuid(), "Apple", false, priority: 1);
var c = new TodoItem(Guid.NewGuid(), "zzz", false, priority: 3);

var repo = new InMemoryTodoReadRepository(new[] { a, b, c });
var sut = new TodoQueries(repo);

// Act 🚀
var result = sut.GetAll();

// Assert ✅
Assert.Equal(new[] { c.Id, b.Id, a.Id }, result.Select(x => x.Id));
}
}

🎉 これだけ! 「優先度3が先」「同じ優先度ならタイトル昇順(大文字小文字無視)」が一発で検証できる✨


7-2) 絞り込みテスト(完了だけ)✅🧹

[Fact]
public void GetCompleted_should_return_only_completed_items()
{
// Arrange 🧸
var done = new TodoItem(Guid.NewGuid(), "done", true, 1);
var todo = new TodoItem(Guid.NewGuid(), "todo", false, 1);

var repo = new InMemoryTodoReadRepository(new[] { done, todo });
var sut = new TodoQueries(repo);

// Act 🚀
var result = sut.GetCompleted();

// Assert ✅
Assert.Single(result);
Assert.True(result[0].IsCompleted);
Assert.Equal(done.Id, result[0].Id);
}

7-3) 検索テスト(大文字小文字無視)🔎✨

検索は境界が多いので Theory が相性最高!😍

[Theory]
[InlineData("app", 2)]
[InlineData("APP", 2)]
[InlineData("le", 2)]
[InlineData("zzz", 0)]
public void SearchByTitle_should_find_case_insensitively(string keyword, int expectedCount)
{
// Arrange 🧸
var items = new[]
{
new TodoItem(Guid.NewGuid(), "Apple", false, 1),
new TodoItem(Guid.NewGuid(), "PineApple", false, 1),
new TodoItem(Guid.NewGuid(), "Banana", false, 1),
};

var repo = new InMemoryTodoReadRepository(items);
var sut = new TodoQueries(repo);

// Act 🚀
var result = sut.SearchByTitle(keyword);

// Assert ✅
Assert.Equal(expectedCount, result.Count);
}

7-4) 空文字・空白は0件にする(境界テスト)🧱🫙

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void SearchByTitle_should_return_empty_when_keyword_is_blank(string? keyword)
{
// Arrange 🧸
var repo = new InMemoryTodoReadRepository(new[]
{
new TodoItem(Guid.NewGuid(), "Apple", false, 1),
});
var sut = new TodoQueries(repo);

// Act 🚀
var result = sut.SearchByTitle(keyword ?? "");

// Assert ✅
Assert.Empty(result);
}

8) Visual Studioでテスト実行&デバッグ🛠️🪲

Test Explorerでやること😊

  • テスト一覧を見る👀
  • 実行する▶️
  • 失敗したテストを絞り込む🔍
  • テストをデバッグする🐛

Test Explorerは公式でも「ユニットテストを効率よく回す場所」として案内されてるよ✨ (Microsoft Learn) デバッグも、Test Explorerから直接できる(ブレークポイント置いて追える)って明記されてる👍 (Microsoft Learn)

さらに嬉しい:VS 2026の “テスト失敗→Copilotでデバッグ” 🚀🤖

Visual Studio 2026 だと、失敗テストを右クリックして Copilotと連携してデバッグを進める流れ まで入ってきてるよ✨ (Microsoft Learn) (ただし、最終判断は人間がやろうね🫶)


9) コマンドラインでも回せる(dotnet test)⌨️✅

Visual Studio以外でも、これで回せます👇

  • dotnet test

最新の dotnet test.NET 10 SDK以降での改善も入ってるので、CIでも超便利だよ〜✨ (Microsoft Learn)


10) VS Code派の人向け(C# Dev Kitでテスト)🧩🧪

VS Codeなら C# Dev Kit のテスト機能で discovery & 実行ができるよ😊 (Visual Studio Code) (ただ、困ったら dotnet test に逃げるのが最強の保険✨)


11) AIでテストケース案を出すコツ🤖📝(ラクして品質UP)

11-1) Copilot “Testing for .NET” を使う(Visual Studio)🧪🤖

GitHub Copilot Chat には テスト生成の機能が入ってて、xUnit / NUnit / MSTest を選んで作れるよ✨ (Microsoft Learn) たとえば Chat にこう打つやつ👇(公式の例)

  • @test #target(solution/project/file/class/member を指定) (Microsoft Learn)

11-2) でも!AIに丸投げしないでね⚠️🧷

AIはときどき…

  • 仕様を “それっぽく” 勝手に決める😇
  • テスト名は良いけど中身が雑🥲
  • 重要な境界(空/並び順/大小文字)を落とす💥

だから、AIにはこう頼むのが強いよ💪✨

✅ テストケース案を出させるプロンプト例(コピペ用)

あなたはC#のユニットテスト設計者です。
次のQueryメソッドに対して、重要なテスト観点を列挙してください:
- 空(0件)
- 並び順(ソート)
- 絞り込み(フィルタ)
- 境界(null/空文字/大小文字/存在しないID)
その後、xUnitでテストコードを書いてください。
前提:Queryは副作用なし。テストはインメモリRepoで書くこと。

12) ミニ演習🎮✨(5分でOK)

演習A:テストを1本追加しよう🧪

GetById に対して👇をテストしてみてね!

  • 存在するID → Todoが返る✅
  • 存在しないID → nullが返る🫙

演習B:並び順ルールを変えたくなったら?🔃

もし並び順を 「Priority降順 → IsCompleted(未完了を先)→ Title」 にしたら、どのテストが落ちる?どれを直す?😊


13) よくある詰まりポイント🧱😵‍💫(先に潰す!)

  • 😵「テストが発見されない」

    • Test Explorerのフィルタが効いてることがあるよ🔍
    • まずは dotnet test で通るか確認(切り分け最強)⌨️✨ (Microsoft Learn)
  • 😵「並び順の期待値がズレる」

    • OrderBy の条件が複数あるときは “期待する順番を明文化”(ID配列で比較)にすると安定✅
  • 😵「検索が不安定」

    • 大文字小文字、空白、部分一致…境界を先に固定しよう🧱✨

まとめ🎉✨

  • Queryは 副作用がない → テストが 入力→出力 で完結しやすい😍
  • 観点はだいたい 空 / 並び順 / 絞り込み / 境界 の4つで勝てる🏆
  • Visual Studioの Test Explorer で 実行もデバッグも超ラク🛠️🐛 (Microsoft Learn)
  • AIは テスト観点の補助 に使うと強い(丸投げは危険)🤖🧷 (Microsoft Learn)

次の 第12章 はいよいよ、**Commandのテスト(副作用の確認)**に突入だよ〜🎭🧪 「モックってなに?」も、怖くない範囲で“必要最小限”だけ使っていくね😊✨