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

第22章:失敗の種類を増やす(例外のテスト入門)🚫🧪

この章でやることはシンプルだよ〜😊✨ 「うまくいく時」だけじゃなくて、 「うまくいかない時」も仕様としてテストに書けるようになるのがゴールです💪🧁


1) 例外テストって、なんで必要?🤔💥

画像を挿入予定

普通のテスト(正常系)だけだと…

  • 間違った入力が来た時に、どんな失敗をするのが正しいのかが曖昧になりがち😵
  • 結果として「たまたま落ちた」「別の例外で落ちた」が混ざって、バグなのか仕様なのか分からない…😭

だから、失敗も仕様として固定するよ📌✨ 特に「引数が変」「状態が変」は、APIを使う側にとって超大事!


2) まず覚える“例外3兄弟”👭✨(超入門)

✅ ArgumentNullException(nullはダメ!)

「null が来たらダメ」をはっきり伝える例外だよ🙂 ThrowIfNull が便利で、引数名もいい感じに入れてくれるのがポイント✨ (Microsoft Learn)

✅ ArgumentException / ArgumentOutOfRangeException(値が変!範囲外!)

  • ArgumentException:形式がダメ(空文字、条件違反など)
  • ArgumentOutOfRangeException:範囲がダメ(負数、上限超えなど)

ArgumentExceptionParamName で「どの引数が悪いか」が表せるよ📝 (Microsoft Learn)

✅ InvalidOperationException(状態がダメ!)

「今その操作しちゃダメ(状態的に)」のときに使う候補だよ🚦


3) xUnitで例外をテストする基本セット🧪✨

Assert.Throws<T>:**“その型ぴったり”**を期待する

xUnitの Throws派生型ではダメで、完全一致を期待するよ📌 (api.xunit.net)

Assert.ThrowsAny<T>“その型 or 派生型OK”

「細かい型は今は気にしない(派生も許可)」ならこっち😊 (api.xunit.net)

Assert.ThrowsAsync<T>:async版(Func<Task> を渡す)

非同期メソッドなら ThrowsAsyncFunc<Task> を渡して、await するのがコツだよ⚡ (api.xunit.net)

Record.Exception:例外を“録画”して、あとで見る

「例外が出る/出ない」だけじゃなく、例外の中身を見て追加検証したいときに便利👀 (api.xunit.net)


4) ハンズオン:カフェ会計に“無効入力”を追加する☕️🧾🚫

今日の題材:クーポン適用メソッド🎟️✨

仕様(まずは超ミニ)👇

  • codenullArgumentNullException
  • code空/空白ArgumentException
  • subtotal負数ArgumentOutOfRangeException
  • code が知らない値 → ArgumentException(今は単純にこれでOK)

5) Red → Green → Refactor で作るよ🚦✨

✅ Step 1: Red(落ちるテストを書く)🔴

using Xunit;

public class CouponServiceTests
{
[Fact]
public void ApplyCoupon_CodeIsNull_ThrowsArgumentNullException()
{
// Arrange
string? code = null;

// Act & Assert
Assert.Throws<ArgumentNullException>("code", () =>
CouponService.ApplyCoupon(code!, 100m));
}

[Fact]
public void ApplyCoupon_CodeIsWhitespace_ThrowsArgumentException()
{
// Arrange
var code = " ";

// Act & Assert
Assert.Throws<ArgumentException>("code", () =>
CouponService.ApplyCoupon(code, 100m));
}

[Fact]
public void ApplyCoupon_SubtotalIsNegative_ThrowsArgumentOutOfRangeException()
{
// Arrange
var code = "OFF10";

// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>("subtotal", () =>
CouponService.ApplyCoupon(code, -1m));
}

[Fact]
public void ApplyCoupon_UnknownCode_ThrowsArgumentException()
{
// Arrange
var code = "???";

// Act & Assert
var ex = Assert.Throws<ArgumentException>("code", () =>
CouponService.ApplyCoupon(code, 100m));

// 例外メッセージまで固定したい時だけ(今回は例として)
Assert.Contains("unknown", ex.Message, StringComparison.OrdinalIgnoreCase);
}
}

ポイント💡

  • Assert.Throws<ArgumentException>("code", ...) みたいに paramName まで検証できるの、地味に強いよ🧷 (xUnitの ThrowsArgumentException 派生なら paramName を見れる形がある) (api.xunit.net)
  • メッセージ検証は 壊れやすいから、基本は「型+paramName」くらいが安定🥹✨

✅ Step 2: Green(最小実装で通す)🟢

public static class CouponService
{
public static decimal ApplyCoupon(string code, decimal subtotal)
{
ArgumentNullException.ThrowIfNull(code); // nullチェックはこれが気持ちいい✨

if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("Coupon code must not be empty.", nameof(code));

if (subtotal < 0)
throw new ArgumentOutOfRangeException(nameof(subtotal), "Subtotal must be >= 0.");

if (!string.Equals(code, "OFF10", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Unknown coupon code.", nameof(code));

// OFF10: 10% OFF(仮)
return subtotal * 0.9m;
}
}

ここでの学び🎀

  • ThrowIfNull は「paramName を自分で渡さないのが推奨」って話があるよ(引数名をうまく拾ってくれるため) (Microsoft Learn)
  • 引数チェックは asyncの“前”に済ませるのがおすすめ(非同期に入ってから投げると追跡がダルくなることある) (Microsoft Learn)

✅ Step 3: Refactor(読みやすく整える)🧼✨

この例だと、いまの時点で無理に抽象化しなくてOK😊 ただ、増えてきたらこうするのはアリ👇

  • クーポン判定を IsKnownCoupon(code) に分ける
  • 割引計算を ApplyOff10(subtotal) に分ける

でも今日は 「例外も仕様」 が主役だから、やりすぎ禁止〜🍰🙅‍♀️


6) 例外テストの“よくある事故”あるある😵‍💫🧨

  • Assertの外でメソッド呼んじゃう → 例外が先に飛んでテストが落ちる(Assert.Throws のラムダの中で呼ぶ!)
  • ✅ asyncなのに Throws を使う / await しない → 正しく検証できない(Assert.ThrowsAsyncawait!) (api.xunit.net)
  • ✅ なんでも ThrowsAny<Exception> にしがち → 仕様がユルユルになって、バグを見逃しやすい😂(最初は型をちゃんと決めよ)

7) AIの使いどころ(この章向け)🤖💡

AIはここでめちゃ便利✨ ただし 採用はテストが主役 ね😊✅

使えるプロンプト例🎀

  • 「このメソッドにありそうな無効入力パターンを、正常/異常/境界値で列挙して」
  • 「無効入力ごとに、どの例外型が自然か候補を出して(理由つき)」
  • 「このテスト、paramName まで検証した方がいいケースある?」

8) ミニ確認問題(3分)📝✨

Q1️⃣ Assert.Throws<ArgumentException> で、ArgumentOutOfRangeException(派生)が投げられたらどうなる? →(ヒント:Throws は“完全一致”) (api.xunit.net)

Q2️⃣ 「派生でもOK」にしたいときはどれ? →(ヒント:名前に Any がある) (api.xunit.net)

Q3️⃣ ThrowIfNull を使うとき、paramName を自分で渡さないのが推奨なのはなぜ? (Microsoft Learn)


9) この章の“提出物”(おすすめコミット単位)📦✨

  • test: add exception specs for ApplyCoupon (null/blank/negative/unknown) 🧪
  • feat: implement ApplyCoupon argument validation and OFF10 🧩
  • refactor: tidy validation messages and ordering 🧼

おまけ:今日の“最新メモ”🗞️✨

  • .NET 10 は 2026/01/13 のアップデートで 10.0.2 が案内されてるよ🪟 (GitHub)
  • xUnit v3 の xunit.v3 パッケージは 3.2.2 が見えてるよ🧪 (nuget.org) (教材の中身はバージョンに強く依存しない書き方にしてあるから安心してね😊)

次は第23章の「テストが設計の警報器👃🚨」に入る準備ができた感じだよ〜! もしこの章の題材を「カフェ会計②(割引・クーポン・上限)」側に寄せて、割引ルールを増やす版に作り替えたいなら、それ用の課題設計もすぐ出せるよ☕️🎟️✨