第23章:“におい”を嗅ぐ(テストが設計の警報器)👃🚨
この章の結論:テストがつらい=あなたの根性不足じゃないです😂 それはだいたい 「設計をちょい整えるタイミングだよ〜」 っていう警報です🚨✨(しかも超ありがたい)
今日の“最新メモ”🗓️✨(本日時点のリサーチ反映)
- Microsoft公式の ユニットテスト Best Practices は 2025/03/22 更新で、AAA(Arrange/Act/Assert)などの基本が整理されています🧱🧪 (Microsoft Learn)
- Visual Studio の Test Explorer 公式ガイドは 2025/11/25 更新で、AAAの説明も含めて手順がまとまっています🪟🔍 (Microsoft Learn)
- xUnit は v3 の Getting Started / What’s new が 2025/08 更新で公開されています🧪✨ (xunit.net)
- xUnit には 公式アナライザー規則一覧があり、Theory/InlineData などのありがちな事故を静かに防いでくれます🧯 (xunit.net)
- テストの “におい(Test Smells)” は xUnit Test Patterns(Meszaros) にまとまった代表例があります👃 (xunitpatterns.com)
1) この章でできるようになること🎯✨

ゴール✅
- テストを書いていて 「つらっ😇」 ってなったときに、どの種類の“におい”か を言語化できる
- “におい”から 「設計のどこが苦しいのか」 を推理できる
- 最小のリファクタで、テストも実装もラクにできる(大改造しない🧸)
2) 「におい」って何?🤔👃
におい=表面の症状です👃
- 例:Arrangeが長すぎる、モックだらけ、テストがたまに落ちる、遅い、何が壊れたか分からない…
- それはたいてい 設計のどこかに原因があります(責務が混ざってる、依存が漏れてる、I/Oが中心に入り込んでる…)📦🧩
コードやテストの“smell”は「たぶん奥に原因があるよ」というサイン、って考え方がよく使われます🐾 (scg.unibe.ch)
3) 代表的な “テストのにおい” 図鑑📚👃✨
ここは「まず覚えると一生使える」やつだけに絞るね😊 (テストのにおい一覧の考え方は xUnit Test Patterns にまとまっています👃) (xunitpatterns.com)
A. Arrange がデカすぎる(準備が重い)🏋️♀️📦
症状
- Arrange が 20行、Act が 1行、Assert が 1行😇
- オブジェクト生成が連鎖して「何をテストしてるの?」ってなる
だいたいの原因(設計側)
- 1クラスに責務が集まりすぎ(神クラス👑)
- “必要な情報”が遠くにあって、セットアップが大工事
効く処方箋(最小)
- テストデータを 小さく作る(値を減らす🧸)
- “そのテストに必要なもの”だけを引数で渡す(後の章でDIに進化するやつ✨)
B. テストが壊れやすい(実装をちょっと変えたら全滅)🧨
症状
- リファクタしただけなのにテストが大量死…😭
- “振る舞い”ではなく “内部手順” を検証している
原因
- 実装詳細に依存してる(privateな動きとか、呼び出し順とか)
処方箋
- Assert を 結果(出力・状態・副作用) に寄せる
- AAAで「何をした」「何を期待」かを分ける(Microsoftも推奨の定番)🧱 (Microsoft Learn)
C. テストがたまに落ちる(フレーク)🎲🚫
症状
- ローカルだと通るのに、たまに落ちる😵💫
- 実行順で挙動が変わる
原因
- 時間・乱数・並列・共有状態(static等)
- テストが独立してない
処方箋
- “変わるもの”を固定する(時間/乱数)
- 共有状態を消す(または初期化を徹底)
D. どの Assert が落ちたか分からない(Assertion Roulette)🎰😇
症状
- Assert がいっぱい並んでて、落ちた理由が分かりにくい
- 失敗ログが読みづらい
原因
- 1テストに意図が混ざってる(前章の「1テスト1意図」違反🍰🙅♀️)
処方箋
- テストを分割
- Assert を意味ある単位にする(期待を言葉にする)
E. “謎ゲスト”がいる(Mystery Guest)🕵️♀️
症状
- どこからデータが来てるか分からない(ファイル/DB/共有Fixture)
- 失敗時に原因が追いにくい
原因
- 外部資源に寄りかかっている
- Arrange がブラックボックス
処方箋
- テスト内に「必要なデータ」を小さく置く
- データ生成をシンプルに(賢くしすぎない🧸)
4) “におい” → “原因” を当てる早見表🗺️👃
テストで感じること → 設計の疑い、の対応表だよ😊
- Arrangeが長い → 責務が混ざってる / 依存が多い / 入力が作りづらい
- モックが多い → クラスがやりすぎ / 境界が曖昧 / 依存が中心に入り込んでる
- ちょいリファクタでテスト爆発 → 実装詳細をテストしてる
- たまに落ちる → 時間/乱数/共有状態/順序依存
- 遅い → I/O混入(DB/ネット/ファイル)や巨大データ
- private を直接テストしたくなる → 公開APIが“意図”を表してない or 責務の切り方が変
5) ハンズオン:つらいテストを“警報器”として使う🚨🧪✨
ここでは わざと「つらい」テストを作って、 「におい」→「原因」→「最小リファクタ」までやります😊🎮
お題:カフェ会計(割引つき)☕️🎟️🧾
仕様(超ざっくり):
- 小計に対して割引(%)を適用
- 合計は 0円未満にならない
- 割引率が不正(負数・100超)は例外
Step 1:つらい版(においが出る形)😇
「計算」なのに、クラスが色々抱えてる想定👇
public sealed class CheckoutService
{
private readonly ILogger _logger;
private readonly ICampaignRepository _campaigns;
private readonly IReceiptPrinter _printer;
public CheckoutService(ILogger logger, ICampaignRepository campaigns, IReceiptPrinter printer)
{
_logger = logger;
_campaigns = campaigns;
_printer = printer;
}
public decimal Checkout(decimal subtotal, string campaignCode)
{
var campaign = _campaigns.FindByCode(campaignCode); // 外部依存
if (campaign.DiscountRate < 0 || campaign.DiscountRate > 100)
throw new ArgumentOutOfRangeException(nameof(campaign.DiscountRate));
var discounted = subtotal * (100 - campaign.DiscountRate) / 100m;
if (discounted < 0) discounted = 0;
_logger.Info("checkout"); // 副作用
_printer.PrintReceipt(discounted); // 副作用
return discounted;
}
}
**この時点での“におい”**👃🚨
- ただの割引計算なのに、外部依存 + 副作用 が混ざってる
- テストで「計算」だけ見たいのに、周辺が邪魔する
Step 2:テストがつらい(症状を観察)🔍
public class CheckoutServiceTests
{
[Fact]
public void Checkout_10PercentOff_ReturnsDiscountedTotal()
{
// Arrange(長い)
var logger = new FakeLogger();
var campaigns = new FakeCampaignRepository(new Campaign("OFF10", 10));
var printer = new FakeReceiptPrinter();
var sut = new CheckoutService(logger, campaigns, printer);
// Act
var total = sut.Checkout(1000m, "OFF10");
// Assert(本当は計算だけ見たいのに…)
Assert.Equal(900m, total);
Assert.True(logger.Called);
Assert.Equal(900m, printer.LastPrintedTotal);
}
}
ここで言語化しよう😊✨
- におい:Arrangeが重い/副作用確認が混ざる/テストの意図が散る
- 警報:「計算」と「周辺処理(ログ・印刷・リポジトリ)」の責務が混ざってるかも🚨
Step 3:最小リファクタ(“計算”を外に出す)🧩✨
狙いはこれ👇
- 計算=純粋ロジックにして、テストを軽くする🧪
- 周辺(取得・ログ・印刷)は後で別枠テストにする
public static class DiscountCalculator
{
public static decimal ApplyRate(decimal subtotal, int discountRate)
{
if (discountRate < 0 || discountRate > 100)
throw new ArgumentOutOfRangeException(nameof(discountRate));
var discounted = subtotal * (100 - discountRate) / 100m;
return discounted < 0 ? 0 : discounted;
}
}
テストはこうなる👇(急に平和🕊️✨)
public class DiscountCalculatorTests
{
[Theory]
[InlineData(1000, 10, 900)]
[InlineData(200, 0, 200)]
[InlineData(500, 100, 0)]
public void ApplyRate_ReturnsExpected(decimal subtotal, int rate, decimal expected)
{
var total = DiscountCalculator.ApplyRate(subtotal, rate);
Assert.Equal(expected, total);
}
[Theory]
[InlineData(-1)]
[InlineData(101)]
public void ApplyRate_InvalidRate_Throws(int rate)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
DiscountCalculator.ApplyRate(1000m, rate));
}
}
xUnit の [Theory] / [InlineData] の基本は v3 Getting Started にも載っています🧪 (xunit.net)
(アナライザー規則もあるので、ミスが減ります🧯) (xunit.net)
Step 4:このリファクタで何が起きた?🌈
- 「つらさ」の原因だった 責務の混在がほぐれた🧩
- テストが AAA っぽく読みやすくなった(AAAは公式でも基本として説明されています)🧱 (Microsoft Learn)
- しかも、仕様(割引率の範囲)もテストで固定できた🧪✨
ポイント:この章では “大改造”じゃなく「計算を外に出す」だけ。これがちょうどいい最小手術💉😊
6) AIの使いどころ(この章の勝ちパターン)🤖✅✨
AIは“設計の匂い当て”が得意です👃 でも 採用判断はあなた(テストが通って、意図に合うか)✅
コピペ用プロンプト例📝
- 「このテストがつらい理由(におい)を3つに分類して。原因になりそうな設計の問題もセットで」
- 「“最小の変更”でテストが軽くなるリファクタ案を3つ。各案のリスクも」
- 「この仕様から、境界値・異常系のテストケースを列挙して」
7) よくある失敗あるある😇💥(回避!)
- においがしたからって、いきなり抽象化しすぎる(インターフェース増殖🌱🌱🌱) → この章は「最小の分離」まででOK😊
- テストを“実装の写し”にしちゃう → 振る舞い(結果)を見よう🧪
- 共通化しすぎてテストが読めなくなる → “読み物”が最優先📘✨(AAAの意図もそこ) (Microsoft Learn)
8) 章末チェックリスト✅👃🚨
あなたのテストで、当てはまったら“警報”:
- Arrange が長すぎて何をテストしてるか曖昧
- モック/スタブが多すぎて、意図がぼやける
- たまに落ちる(時間/乱数/順序/共有状態)
- リファクタしただけでテストが大量死
- Assertが多すぎて、失敗理由が一瞬で分からない
- private を直接テストしたくなる衝動がある
→ 2個以上なら、設計をちょい整えると幸福度が上がる可能性大😊✨
9) 今日のミニ宿題🎒🧪
- 自分のテストで「つらい」やつを1本選ぶ👀
- この章のチェックリストで “におい”を3つ言語化する👃📝
- 最小リファクタを1つだけやる(例:計算を外に出す、入力を引数で渡せる形にする)🧩
- テストが「短く・読みやすく」なったかを確認✅✨
次の第24章は、この章で嗅ぎ当てた“警報”を 安全運転でリファクタする手順に落とし込む回だよ🛡️🧹✨