第23章:単体テスト②(網羅の考え方)🎯🧪✨
(テーマ:全部テストできないとき、どう“賢く”テストする?)
第22章で「遷移表の行=テストケース」にできたよね?😆✨ でも現実は…状態もイベントも増えるから、全部の組み合わせをテストするのはムリになりがち💦 だから第23章は、“網羅”の考え方と、優先度A/B/Cでテスト計画を作る技術を身につけるよ〜!🫶🎯
1) 今日のゴール(できるようになること)🎓✨
- 「網羅」って言われてもビビらない😎
- 状態機械のテストで大事な“網羅軸”が分かる🧭
- 優先度A/B/Cで、現実的なテスト計画を作れる📋✨
- コードカバレッジ(行カバレッジ)を“使い方限定”で活かせる🧪📈 ([Microsoft Learn][1])
2) まず「網羅」って何?👀✨

「網羅=全部やる」じゃないよ🙅♀️💦 網羅=“何をどこまで確認したいか”の観点(軸)を決めて、漏れを減らすことだよ✅✨
状態機械だと、網羅の軸がいくつかあるのがポイント🌟
3) 状態機械テストの“網羅軸”ベスト6 🏆🧪
ここが第23章の核だよ〜!💖
A. 状態カバレッジ(State Coverage)🧊
- 全状態に1回は到達できてる?
- 「その状態に入ったときのルール(不変条件)」壊れてない?
B. 遷移カバレッジ(Transition Coverage)🔁
- 全ての“矢印(遷移)”を1回は通す
- 状態機械ではコレが超重要🎯
C. イベントカバレッジ(Event Coverage)📣
- 全イベントが、少なくともどこかで期待通り動く?
- 逆に「どの状態で受け付けないか(禁止)」も確認できてる?🚫
D. ガード条件カバレッジ(Guard Coverage)🛡️
- 条件つき遷移は 境界(ギリギリ) が事故りやすい😵💫
- 例:Cancelは「調理開始前だけ」→ 開始前/開始後 両方テスト必須🍳
E. 失敗カバレッジ(Failure Coverage)✅❌
- 禁止遷移が「例外で落ちる」じゃなく、Resultで安全に返る?
- UI/APIに渡す理由コード・メッセージが正しい?💬✨
F. 重要パス集中(Critical Path Coverage)🚀
- **一番使われる流れ(ハッピーパス)**を最優先で守る
- “売上・支払い・返金”みたいな致命傷ルートはA級🔥💳
4) 「全部テストできない」時の優先順位の決め方🧠✨
ここからが実務力〜!💪😆
優先度はだいたいこの3つの掛け算で決めると強いよ👇
- 壊れやすさ:分岐多い/境界ある/複雑/過去にバグった?🧨
5) 演習:テスト計画をA/B/Cで作ろう📋✨
題材:「学食モバイル注文」🍙📱 状態:Draft → Submitted → Paid → Cooking → Ready → PickedUp 例外:Cancelled / Refunded
✅ 優先度A(落としたら炎上🔥)
- 支払い(Paid)に関係する遷移:Submitted→Paid、Paid→Refunded など💳
- ハッピーパスの骨格:Draft→Submitted→Paid→Cooking→Ready→PickedUp 🚀
- ガード境界:Cancel可能/不可能の境目(調理前/後)🍳🛡️
- 二重実行しそうなイベント(Pay連打など)は次章で扱うけど、最低限の防波堤はここで意識👆💥
👍 優先度B(大事だけど、Aの次)
- キャンセル系(Draft/SubmittedでのCancel)
- ReadyからPickUpまでの例外(受け取り期限などがあるなら)⏰
- メッセージ内容(丁寧さ違い)は第20章寄りだけど、最低限は確認💬
🥺 優先度C(余裕があれば)
- レアな運用(管理者が手動で戻す等)
- ほぼ起きない入力(極端な金額など)※ただし“境界”ならA/Bに昇格🎯
6) 「網羅したつもり」事故あるある😵💫(回避策つき)
あるある①:行カバレッジ100%で安心しちゃう📈💦
- 行カバレッジは「通った行」しか見ないよ
- 状態機械では “遷移(矢印)”を通したか を主役にしてね🔁🎯
あるある②:禁止遷移をテストしてない🚫
- 事故の半分は「想定外の状態でボタン押された」系👆💥
- 禁止遷移は “仕様” だから、ちゃんとテスト対象だよ✅
7) テストコード例(xUnitで“優先度Aの骨格”)🧪✨
※ここでは「遷移の判断だけ」をテストしやすい形(副作用は別)にしてる前提だよ🚪✨
using Xunit;
public enum OrderState
{
Draft, Submitted, Paid, Cooking, Ready, PickedUp,
Cancelled, Refunded
}
public enum OrderEvent
{
Submit, Pay, StartCooking, MarkReady, PickUp, Cancel, Refund
}
public readonly record struct TransitionResult(bool Ok, OrderState State, string Reason);
public static class OrderStateMachine
{
public static TransitionResult Apply(OrderState state, OrderEvent ev, bool cookingStarted = false)
{
// 例:Cancelは「調理開始前だけOK」みたいなガード
if (ev == OrderEvent.Cancel)
{
if (state is OrderState.Draft or OrderState.Submitted && !cookingStarted)
return new(true, OrderState.Cancelled, "OK");
return new(false, state, "CANCEL_NOT_ALLOWED");
}
return (state, ev) switch
{
(OrderState.Draft, OrderEvent.Submit) => new(true, OrderState.Submitted, "OK"),
(OrderState.Submitted, OrderEvent.Pay) => new(true, OrderState.Paid, "OK"),
(OrderState.Paid, OrderEvent.StartCooking) => new(true, OrderState.Cooking, "OK"),
(OrderState.Cooking, OrderEvent.MarkReady) => new(true, OrderState.Ready, "OK"),
(OrderState.Ready, OrderEvent.PickUp) => new(true, OrderState.PickedUp, "OK"),
(OrderState.Paid, OrderEvent.Refund) => new(true, OrderState.Refunded, "OK"),
_ => new(false, state, "INVALID_TRANSITION")
};
}
}
```mermaid
flowchart LR
S1[Submitted] -- "Cancel<br/>(始まってない)" --> OK[Cancelled ✅]
S1 -- "Cancel<br/>(調理開始後)" --> NG[Rejected 🚫]
subgraph Guard ["ガード条件の境界テスト"]
direction TB
B["調理開始フラグ"]
B -- false --> OK
B -- true --> NG
end
public class OrderStateMachineTests { [Fact] public void A_HappyPath_should_reach_PickedUp() { var s = OrderState.Draft;
s = OrderStateMachine.Apply(s, OrderEvent.Submit).State;
s = OrderStateMachine.Apply(s, OrderEvent.Pay).State;
s = OrderStateMachine.Apply(s, OrderEvent.StartCooking).State;
s = OrderStateMachine.Apply(s, OrderEvent.MarkReady).State;
s = OrderStateMachine.Apply(s, OrderEvent.PickUp).State;
Assert.Equal(OrderState.PickedUp, s);
}
[Theory]
[InlineData(OrderState.Draft, false, true)]
[InlineData(OrderState.Submitted, false, true)]
[InlineData(OrderState.Paid, false, false)]
[InlineData(OrderState.Submitted, true, false)] // 調理開始後はCancel禁止のイメージ
public void A_Cancel_guard_boundary(OrderState state, bool cookingStarted, bool shouldOk)
{
var r = OrderStateMachine.Apply(state, OrderEvent.Cancel, cookingStarted);
Assert.Equal(shouldOk, r.Ok);
if (!shouldOk) Assert.Equal("CANCEL_NOT_ALLOWED", r.Reason);
}
}
ポイント💡
* **A(炎上防止)**は「ハッピーパス」「支払い」「境界ガード」をまず固める🔥💳🛡️
* 禁止遷移の `Reason` を見るテストは、仕様の“言い切り”になって強いよ✅✨
---
## 8) コードカバレッジの取り方(CLI/Visual Studio)📈🧪
### ✅ CLIで取る(簡単&CIでも使える)
`dotnet test --collect:"XPlat Code Coverage"` でOK✨
XPlat Code Coverage は coverlet のデータコレクターに対応してて、結果(例:cobertura.xml)が TestResults に出るよ🧾✨ ([Microsoft Learn][1])
### ✅ Visual Studioで取る(見た目が分かりやすい)
Visual Studio 2026 では Test メニューから **Analyze Code Coverage for All Tests** などで実行できて、エディタ上で“どこ通ってるか”も見えるよ👀✨ ([Microsoft Learn][2])
⚠️注意:
* カバレッジは“高いほど正しい”じゃないよ🙅♀️
* 状態機械は **遷移(矢印)ベースの網羅**を主にして、カバレッジは「穴探し補助」くらいがちょうどいい🔍✨
---
## 9) AI活用(第23章の使いどころ)🤖✨
### ① 優先度A/B/Cの叩き台を作らせる📋
* 「遷移表(状態×イベント→次状態)」を渡して
* “影響度/頻度/壊れやすさ”の観点でスコア付け
* A/B/Cに分類
* Aだけ先に最小ケース数に圧縮
みたいに頼むと強いよ🔥
### ② “落ちやすい組み合わせ”を推測させる🔮
* ガード条件の境界
* 例外系(Refund/Cancel)
* 条件の漏れ(SubmittedでRefundしてない?等)
を列挙してもらうと、人間の見落としが減る🫶✨
### ③ テストの雛形(Theory/InlineData)を生成させる🧪
ただし!
* **期待値(次状態・理由コード)は人間が最終責任**で確定してね✅(AIはここを平気で間違える💦)
---
## 10) つまずきポイント集(先回り)🧯✨
* **テスト名が雑** → `A_何を守る_どんな条件で_どうなる` みたいに命名すると読みやすい📛✨
* **Theoryが増えすぎ** → まずA級だけTheory化、B/Cは後回し🧹
* **カバレッジの数字に支配される** → “矢印を守れてる?”に戻る🔁🎯
---
## 11) 仕上げチェックリスト✅✨
* [ ] ハッピーパスがAで通る🚀
* [ ] 支払い/返金など“お金系”がAで守れてる💳
* [ ] ガード境界(できる/できない両側)をテストした🛡️
* [ ] 禁止遷移が「落ちないで」理由付きで返る✅❌
* [ ] カバレッジは穴探しに使った(数字を目的化してない)🔍📈
---
## 次章へのつながり(チラ見せ)👀✨
第24章は、支払いAPIみたいな **“待ち”がある世界(async/await)** に入っていくよ⏳⚡
そのとき「中間状態」や「完了イベント」が出てきて、テスト観点も増えるから…
第23章の **優先順位づけ** がめっちゃ効いてくるのだ🫶✨
---
必要なら、この第23章の内容を“授業スライド風”にして、
* 例題(A/B/C分類クイズ🎯)
* 解答と理由(影響度×頻度×壊れやすさ)
* 宿題テンプレ(テスト計画シート📋)
まで付けた版も作れるよ〜😆📘✨
[1]: https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-code-coverage?utm_source=chatgpt.com "単体テストにコードカバレッジを使用する - .NET"
[2]: https://learn.microsoft.com/en-us/visualstudio/releases/2026/release-notes?utm_source=chatgpt.com "Visual Studio 2026 Release Notes"