第50章:卒業制作:推し活グッズ管理③(Blazorで完成)🎓🎉
この章は「TDDを最初から最後まで、自力で回して“完成まで持っていく”」のゴール回です💪✨ (本日時点:.NET 10 の最新は 10.0.2(2026/01/13 リリース) だよ📌)(Microsoft) C# 14 は Visual Studio 2026 / .NET 10 SDK で使える前提だよ🧁(Microsoft Learn)
1) 今回作るアプリ(完成イメージ)🎀📦

必須(これで「完成」)✅
- グッズ登録(名前/カテゴリ/価格/数量/購入日/メモ)📝
- 一覧表示(検索・絞り込み・並び替え)🔎↕️
- 状態変更(在庫あり→予約→売却 みたいに遷移)🧠🗺️
- 集計(総数、カテゴリ別数、総額など)📊✨
- 重要導線だけ UI テスト(bUnit)🧪🖼️
- README(実行方法・テスト方法・仕様)📘
余裕があれば(拡張)🌈
- JSON保存/読み込み(永続化)💾
- タグ機能🏷️
- 画像URL(表示だけ)🖼️
- エクスポート(CSV)📤
2) テスト戦略(ここ大事)🧪✨
- ドメイン(ルール):テスト多め(速い・壊れやすい所を守る)⚡
- ユースケース(アプリ操作):必要十分(登録/検索/遷移/集計)🎯
- UI(Blazor):重要導線だけ(「追加できる」「一覧が更新される」など)🚶♀️
xUnit は v3 系が安定版として継続リリースされてるよ(例:xunit.v3 3.2.2)。(xunit.net) Blazor コンポーネントテストは bUnit を使う想定(NuGet で配布)。(bUnit)
3) ソリューション構成(迷子防止マップ)🗺️😊
「UIは薄く、ルールは外へ」🧩✨
OshiGoods.sln
├─ src
│ ├─ OshiGoods.Web // Blazor(画面)
│ ├─ OshiGoods.App // ユースケース(登録/検索/集計)
│ └─ OshiGoods.Domain // ドメイン(ルール・型・状態遷移)
└─ tests
├─ OshiGoods.DomainTests
├─ OshiGoods.AppTests
└─ OshiGoods.WebTests // bUnit(最小)
4) 仕様を「テストに落とす」やり方(卒業制作のコツ)🎯🧪
まずはユースケースを4つに絞る🍰
- 登録できる
- 検索/絞り込みできる
- 状態遷移できる(例:在庫→予約→売却)
- 集計できる
この4つだけで十分“完成感”が出るよ🎉
5) 開発の進め方(おすすめコミット単位)🧠🔁
ステップA:ドメインから作る(ルールが最優先)👑
A-1 価格・数量の「型」から
- Money(0以上)
- Quantity(1以上)
A-2 状態遷移(状態機械の超ミニ版)
- InStock → Reserved → Sold
- Sold → InStock は禁止、とかをルールにする🚫
コミット例
test: Money rejects negativefeat: Money value objecttest: status transition rulesfeat: GoodsItem transition methods
6) まずはドメイン最小コード(例)🧷✨
6-1 Value Object(価格・数量)
namespace OshiGoods.Domain;
public readonly record struct Money(decimal Amount)
{
public static Money Of(decimal amount)
{
if (amount < 0) throw new DomainException("価格は0以上だよ💰");
return new Money(amount);
}
}
public readonly record struct Quantity(int Value)
{
public static Quantity Of(int value)
{
if (value <= 0) throw new DomainException("数量は1以上だよ📦");
return new Quantity(value);
}
}
public sealed class DomainException : Exception
{
public DomainException(string message) : base(message) { }
}
6-2 グッズ本体(状態遷移つき)
namespace OshiGoods.Domain;
public enum GoodsStatus { InStock, Reserved, Sold }
public sealed class GoodsItem
{
public Guid Id { get; } = Guid.NewGuid();
public string Name { get; private set; }
public string Category { get; private set; }
public Money Price { get; private set; }
public Quantity Quantity { get; private set; }
public GoodsStatus Status { get; private set; } = GoodsStatus.InStock;
public string Memo { get; private set; } = "";
public GoodsItem(string name, string category, Money price, Quantity quantity)
{
Name = string.IsNullOrWhiteSpace(name) ? throw new DomainException("名前は必須だよ📝") : name;
Category = string.IsNullOrWhiteSpace(category) ? "未分類" : category;
Price = price;
Quantity = quantity;
}
public void Reserve()
{
if (Status != GoodsStatus.InStock) throw new DomainException("在庫がある時だけ予約できるよ🎟️");
Status = GoodsStatus.Reserved;
}
public void Sell()
{
if (Status == GoodsStatus.Sold) throw new DomainException("もう売却済みだよ💦");
Status = GoodsStatus.Sold;
}
public void UpdateMemo(string memo) => Memo = memo ?? "";
}
7) ドメインテスト(まず1本だけ書こう)🧪🚦
using OshiGoods.Domain;
using Xunit;
public class MoneyTests
{
[Fact]
public void Of_rejects_negative()
{
var ex = Assert.Throws<DomainException>(() => Money.Of(-1));
Assert.Contains("0以上", ex.Message);
}
}
🎯ポイント:最初は1テスト1意図でOKだよ🍰 増やすのは後でいくらでもできる😊
8) ユースケース層(登録・検索・集計)📦✨
8-1 リポジトリ(差し替え前提の境界)🚪
using OshiGoods.Domain;
namespace OshiGoods.App;
public interface IGoodsRepository
{
Task AddAsync(GoodsItem item);
Task<IReadOnlyList<GoodsItem>> ListAsync();
Task<GoodsItem?> FindAsync(Guid id);
Task UpdateAsync(GoodsItem item);
Task DeleteAsync(Guid id);
}
8-2 InMemory実装(卒業制作はこれで十分)💾✨
using System.Collections.Concurrent;
using OshiGoods.Domain;
namespace OshiGoods.App;
public sealed class InMemoryGoodsRepository : IGoodsRepository
{
private readonly ConcurrentDictionary<Guid, GoodsItem> _db = new();
public Task AddAsync(GoodsItem item)
{
_db[item.Id] = item;
return Task.CompletedTask;
}
public Task<IReadOnlyList<GoodsItem>> ListAsync()
=> Task.FromResult((IReadOnlyList<GoodsItem>)_db.Values.OrderBy(x => x.Name).ToList());
public Task<GoodsItem?> FindAsync(Guid id)
=> Task.FromResult(_db.TryGetValue(id, out var item) ? item : null);
public Task UpdateAsync(GoodsItem item)
{
_db[item.Id] = item;
return Task.CompletedTask;
}
public Task DeleteAsync(Guid id)
{
_db.TryRemove(id, out _);
return Task.CompletedTask;
}
}
8-3 ユースケース(サービス)例:登録🎀
using OshiGoods.Domain;
namespace OshiGoods.App;
public sealed class GoodsService
{
private readonly IGoodsRepository _repo;
public GoodsService(IGoodsRepository repo) => _repo = repo;
public async Task<Guid> AddAsync(string name, string category, decimal price, int quantity, string memo)
{
var item = new GoodsItem(name, category, Money.Of(price), Quantity.Of(quantity));
item.UpdateMemo(memo);
await _repo.AddAsync(item);
return item.Id;
}
}
9) Blazor(UIは薄く!)🎨🧪
9-1 DI登録(Program.cs イメージ)🔁
using OshiGoods.App;
builder.Services.AddSingleton<IGoodsRepository, InMemoryGoodsRepository>();
builder.Services.AddSingleton<GoodsService>();
9-2 画面は「入力→サービス呼ぶ→再描画」だけにする🪄
- 入力バリデーションは 最低限(空チェックくらい)
- ルールは Domain が守る(例外メッセージを UI で表示)🧯
10) UIテスト(bUnit)最小でOK🧪🖼️
bUnit は「Blazorコンポーネントのテスト用ライブラリ」だよ📌(bUnit)
例:一覧に名前が出ることだけ確認する
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using OshiGoods.App;
using OshiGoods.Domain;
using Xunit;
public class GoodsListTests : TestContext
{
[Fact]
public async Task Shows_added_item_name()
{
Services.AddSingleton<IGoodsRepository, InMemoryGoodsRepository>();
Services.AddSingleton<GoodsService>();
var svc = Services.GetRequiredService<GoodsService>();
await svc.AddAsync("アクスタ", "グッズ", 1500, 1, "");
var cut = RenderComponent<OshiGoods.Web.Components.GoodsList>();
cut.Markup.Contains("アクスタ");
}
}
🎯UIテストは「重要導線だけ」でいいよ!やりすぎるとしんどい😂
11) AIの使い方(卒業制作の最強セット)🤖✨
仕様→テストケース洗い出し
- 「この仕様のテストケースを、正常/異常/境界値で列挙して。状態遷移も含めてね」
失敗ログ→原因切り分け
- 「この失敗ログの原因候補を3つ、確認順に出して」
リファクタ案(最小だけ採用)
- 「重複を減らす最小のリファクタ案を3つ。リスクも添えて」
PRレビュー役
- 「この差分の“仕様漏れ”と“テスト不足”を指摘して」
✅コツ:AIは“案出し係”、採用は テストと意図で決める😌🧪
12) 完成条件(Definition of Done)🏁💪
- テスト:
dotnet testが常に緑✅ - 主要ユースケース(登録/検索/遷移/集計)がテストで守られてる🧪
- UIは重要導線だけ自動テスト済み(bUnit)🖼️
- READMEに「起動」「テスト」「仕様」が書いてある📘
- コードが怖くない(名前・責務・重複が最低限整ってる)🧹✨
13) よくあるハマり(先に回避)🧯😵💫
- UIにルールを書き始める(→ドメインへ追い出そ!)🚪
- テストが重くなる(→InMemoryで速さ優先!)🐢➡️⚡
- 例外を握りつぶす(→メッセージをUIに見せる or 方針決める)🙅♀️
- 「状態」が増えてif地獄(→遷移表で整理)🗺️
必要なら、この卒業制作を「章内の課題形式」にして、 (1) 1コミット1課題 / (2) 各課題のテストお題 / (3) 期待差分 まで、まるっと講義台本にして出せるよ📘✨