第23章:小さなアプリで総復習(ConsoleでOK)🎮✨
この章は「I/Oを外に出す」を、小さな完成品で体に染み込ませる回だよ〜!🧠💖 今日の時点だと、.NET 10 は最新パッチが **10.0.2(2026-01-13)**で、SDK も 10.0.101 が出てるよ📌 (Microsoft) (そして Visual Studio 2026 も安定版が出てる〜!🎉 2026-01-13 に 18.2.0 が出てるよ) (Microsoft Learn) C# は C# 14 が .NET 10 / Visual Studio 2026 で使えるよ〜🧩 (Microsoft Learn)
23.1 今日つくるミニアプリ:英単語クイズ(ミニ)📚🎯
動きのイメージ👇
- 「開始!」→ 3問出る
- ユーザーが入力
- 正解/不正解を表示
- 最後にスコア表示
ポイントはこれだけ!✨
- 入力/表示(Console)= I/O(外側) 🖥️🚪
- 出題データ(問題リスト)= 交換可能にする 🧩🔁
- 乱数でシャッフル= 境界(IRandom)🎲🚧
- まとめると「テストで全部差し替えできる」🧪🎭
23.2 設計の地図(超ざっくり)🗺️😊


「内側(ルール)」と「外側(I/O)」を分けるよ📦↔️🌍
-
内側(テストしやすい)
- Question(問題)
- QuizGame(進行役。I/Oはインターフェース越し)
- 判定ロジック(文字比較など)
-
外側(本物I/O)
- SystemConsole(Consoleを叩く実装)
- InMemoryQuestionSource(問題の供給)
- DotNetRandom(Randomの実装)
-
組み立て(Composition Root)
- Program.cs に「本物の接続」を寄せる🏗️✨
23.3 ソリューション構成(おすすめ)📦✨
プロジェクトを 3つに分けるよ〜(これが “分離” の練習にちょうどいい!)💪😊
QuizApp.Core(クイズの中身:ロジックとインターフェース)QuizApp.ConsoleApp(コンソール実装:本物I/O)QuizApp.Tests(テスト:Fake/Stubで検証)
CLIで作る場合(VS Code派でもOK)⌨️✨
mkdir QuizApp
cd QuizApp
dotnet new sln -n QuizApp
dotnet new classlib -n QuizApp.Core
dotnet new console -n QuizApp.ConsoleApp
dotnet new xunit -n QuizApp.Tests
dotnet sln add .\QuizApp.Core\QuizApp.Core.csproj
dotnet sln add .\QuizApp.ConsoleApp\QuizApp.ConsoleApp.csproj
dotnet sln add .\QuizApp.Tests\QuizApp.Tests.csproj
dotnet add .\QuizApp.ConsoleApp\QuizApp.ConsoleApp.csproj reference .\QuizApp.Core\QuizApp.Core.csproj
dotnet add .\QuizApp.Tests\QuizApp.Tests.csproj reference .\QuizApp.Core\QuizApp.Core.csproj
Visual Studio派の場合 🧑💻✨
- 「空のソリューション」作成
- プロジェクトを3つ追加(Class Library / Console / xUnit Test)
- 参照(Console→Core、Tests→Core)を追加
23.4 Core(内側)を書く:I/Oは “形” だけにする🧩🚧
① ドメイン:Question
QuizApp.Core に Question.cs を作るよ📄✨
namespace QuizApp.Core;
public sealed record Question(string Prompt, string Answer);
② 境界(インターフェース)を定義
IConsole.cs(入出力の境界)🖥️🚪
namespace QuizApp.Core;
public interface IConsole
{
void WriteLine(string message);
string? ReadLine();
}
IQuestionSource.cs(問題供給の境界)📚🚪
namespace QuizApp.Core;
public interface IQuestionSource
{
IReadOnlyList<Question> GetQuestions();
}
IRandom.cs(乱数の境界)🎲🚪
namespace QuizApp.Core;
public interface IRandom
{
int Next(int maxExclusive);
}
③ 進行役(ユースケース):QuizGame
QuizGame.cs を作るよ🎮✨
「Consoleを直接触らない」で、IConsole 経由にするのがキモ!💡
namespace QuizApp.Core;
public sealed record QuizResult(int Total, int Correct);
public sealed class QuizGame
{
private readonly IConsole _console;
private readonly IQuestionSource _source;
private readonly IRandom _random;
public QuizGame(IConsole console, IQuestionSource source, IRandom random)
{
_console = console;
_source = source;
_random = random;
}
public QuizResult Play(int count)
{
var questions = _source.GetQuestions().ToList();
Shuffle(questions, _random);
var total = Math.Min(count, questions.Count);
var correct = 0;
_console.WriteLine("🎉 ミニ英単語クイズ開始! 🎉");
_console.WriteLine($"全部で {total} 問だよ〜✍️");
for (var i = 0; i < total; i++)
{
var q = questions[i];
_console.WriteLine("");
_console.WriteLine($"Q{i + 1}: {q.Prompt}");
var input = (_console.ReadLine() ?? "").Trim();
if (IsCorrect(input, q.Answer))
{
correct++;
_console.WriteLine("✅ 正解〜!えらいっ👏✨");
}
else
{
_console.WriteLine($"❌ ざんねん!正解は「{q.Answer}」だよ〜😇");
}
}
_console.WriteLine("");
_console.WriteLine($"🎯 結果: {correct}/{total} 正解!");
return new QuizResult(total, correct);
}
private static bool IsCorrect(string input, string answer)
=> string.Equals(input.Trim(), answer.Trim(), StringComparison.OrdinalIgnoreCase);
private static void Shuffle<T>(IList<T> items, IRandom random)
{
// Fisher–Yates shuffle
for (int i = items.Count - 1; i > 0; i--)
{
int j = random.Next(i + 1);
(items[i], items[j]) = (items[j], items[i]);
}
}
}
ここでの嬉しいポイント💖
QuizGameは Console/Random/問題供給を「外からもらう」- だからテストで Fake/Stubに差し替えできる🎭✨
23.5 Console側(外側)を書く:本物I/Oを実装する🔌🖥️
QuizApp.ConsoleApp に実装を置くよ〜!
① SystemConsole(本物Console)
using QuizApp.Core;
namespace QuizApp.ConsoleApp;
public sealed class SystemConsole : IConsole
{
public void WriteLine(string message) => Console.WriteLine(message);
public string? ReadLine() => Console.ReadLine();
}
② 問題の供給:InMemoryQuestionSource
まずはベタ書きでOK(小さく完成させるのが正義💪✨)
using QuizApp.Core;
namespace QuizApp.ConsoleApp;
public sealed class InMemoryQuestionSource : IQuestionSource
{
private static readonly Question[] Questions =
{
new("apple = ?", "りんご"),
new("cat = ?", "ねこ"),
new("book = ?", "ほん"),
new("water = ?", "みず"),
new("sun = ?", "たいよう"),
};
public IReadOnlyList<Question> GetQuestions() => Questions;
}
③ 乱数:DotNetRandom
using QuizApp.Core;
namespace QuizApp.ConsoleApp;
public sealed class DotNetRandom : IRandom
{
private readonly Random _random = new();
public int Next(int maxExclusive) => _random.Next(maxExclusive);
}
23.6 Composition Root(組み立て):Program.cs に集める🏗️✨
Program.cs は「配線するだけ」🔌
ここに “本物” を集めるのがコツだよ〜!📍😊
using QuizApp.Core;
using QuizApp.ConsoleApp;
IConsole console = new SystemConsole();
IQuestionSource source = new InMemoryQuestionSource();
IRandom random = new DotNetRandom();
var game = new QuizGame(console, source, random);
game.Play(count: 3);
console.WriteLine("👋 またね〜!");
これで実行すると、ちゃんとクイズが動くはず!🎉🎮
23.7 テストを書く:Fake差し替えで “速い・安定” を体感🧪⚡
ここが今日のメインイベント🥳✨
QuizApp.Tests に Fake/Stub を置いて、QuizGame をテストするよ!
xUnit について(2026の最新寄り)🧪
xUnit v3 では xunit.v3 がコアで、dotnet test や VS の Test Explorer で回すには xunit.runner.visualstudio を入れるのが定番だよ📌 (xUnit.net)
※ Visual Studio のテンプレが v2 のこともあるけど、仕組みとしては「テストフレームワーク + VSTestアダプタ」って覚えればOK🙆♀️✨
① FakeConsole(入力を流して、出力を記録する)🎭
using QuizApp.Core;
namespace QuizApp.Tests;
public sealed class FakeConsole : IConsole
{
private readonly Queue<string?> _inputs;
public List<string> Outputs { get; } = new();
public FakeConsole(IEnumerable<string?> inputs)
{
_inputs = new Queue<string?>(inputs);
}
public void WriteLine(string message) => Outputs.Add(message);
public string? ReadLine()
=> _inputs.Count == 0 ? null : _inputs.Dequeue();
}
② StubQuestionSource(問題を固定で返す)📌
using QuizApp.Core;
namespace QuizApp.Tests;
public sealed class StubQuestionSource : IQuestionSource
{
private readonly IReadOnlyList<Question> _questions;
public StubQuestionSource(params Question[] questions)
{
_questions = questions;
}
public IReadOnlyList<Question> GetQuestions() => _questions;
}
③ DeterministicRandom(シャッフルを固定化)🎲➡️📌
using QuizApp.Core;
namespace QuizApp.Tests;
public sealed class DeterministicRandom : IRandom
{
private readonly Queue<int> _values;
public DeterministicRandom(IEnumerable<int> values)
{
_values = new Queue<int>(values);
}
public int Next(int maxExclusive)
{
if (maxExclusive <= 0) return 0;
if (_values.Count == 0) return 0;
var v = _values.Dequeue();
if (v < 0) v = -v;
return v % maxExclusive;
}
}
④ テスト本体:正解数が合うかを見る✅
using QuizApp.Core;
using Xunit;
namespace QuizApp.Tests;
public class QuizGameTests
{
[Fact]
public void Play_WhenAnswersAreCorrect_ReturnsCorrectScore()
{
// Arrange
var console = new FakeConsole(new[] { "りんご", "ねこ" }); // 入力2回分
var source = new StubQuestionSource(
new Question("apple = ?", "りんご"),
new Question("cat = ?", "ねこ")
);
// シャッフルで順番が変わらないように 0 連打(実質そのまま)
var random = new DeterministicRandom(new[] { 0, 0, 0, 0 });
var game = new QuizGame(console, source, random);
// Act
var result = game.Play(count: 2);
// Assert
Assert.Equal(2, result.Total);
Assert.Equal(2, result.Correct);
// 出力も “ちょい確認” できる(UIの完全一致はやりすぎ注意⚠️)
Assert.Contains(console.Outputs, x => x.Contains("ミニ英単語クイズ開始"));
Assert.Contains(console.Outputs, x => x.Contains("結果: 2/2"));
}
}
✅ これで、Consoleがなくてもテストが回る! ✅ 乱数が入ってても、揺れない! ✅ 外部環境に依存しない!
これが「I/O境界の分離」の勝ちパターンだよ〜🏆✨
23.8 よくあるミスあるある 😵💫💥(先回りで回避!)
-
ロジックの中で
Console.ReadLine()を直接呼ぶ → テスト不能😇 -
Randomを直接 new して使う → テストが揺れる🌪️
-
テストで UI 文言を完全一致で縛りすぎる → 直したいのに直せない牢獄🫠
- 文字は
Containsくらいがちょうどいいこと多いよ〜😊
- 文字は
23.9 Copilot/Codexの使いどころ🤖💡(この章向け)
おすすめの頼み方(そのままコピペでOK)👇
- 「
IConsoleに対するFakeConsoleを、入力キューと出力ログ付きで作って」🧾 - 「この
QuizGameに対する xUnit のテストを、Arrange/Act/Assert で書いて」🧱 - 「シャッフルがテストで揺れるので、
IRandomを差し替え可能にしたい。最小の設計を提案して」🎲
⚠️ ただし注意! AIが作るコードは、ときどき 境界を破ってくる(平気で Console直呼びに戻す)ので、 「I/Oはインターフェース越し!」って合言葉でチェックしてね🔍✨
23.10 練習問題(ミニ)✍️✨
できそうなのからでOK〜!😊💖
- 問題数をユーザーに選ばせる(でも
ReadLine()は IConsole 経由ね!)🎚️ - 正解時にスコアを +10、不正解は +0 にして、最後に点数表示💯
IQuestionSourceをもう1個作って「ファイルから読み込む版」にしてみる🗂️(第16章の復習✨)- 「もう一回やる?」でループさせる(無限ループ注意😆)
この章のまとめ 🎀✨
- 小さくても「完成品」を作ると、分離の意味が一気に腑に落ちる🎉
- I/Oは外へ、ルールは中へ📦➡️🌍
- Fake/Stub/Deterministicで、テストが 速くて安定になる🧪⚡
次の章(第24章)は、ここまで作った設計を AIにレビューさせるチェックリストにしていくよ🤖✅ このクイズアプリ、そこにピッタリの素材だから、そのまま持っていこう〜!🚀💖