第34章:呼び出し確認(副作用を仕様にする)📣✅
この章はね、「返り値や状態だけじゃ見えない“外部への影響”」をテストで守れるようになる回だよ〜!🥰 例:メール送信📧、通知🔔、ログ🪵、DB保存💾、イベント発行🛰️…みたいなやつ✨
今日のゴール🎯✨

- 「通知した / しなかった」をテストで言い切れるようになる✅
- 呼び出し回数 / 引数 / 呼ばれないことをチェックできるようになる✅
- “やりすぎモック地獄😇”を避けるコツもつかむ✅
まず:副作用ってなに?🌊
副作用=「そのメソッドを呼ぶと、外の世界に影響が出ること」だよ〜🙂
- メール送る📧
- ログに書く🪵
- DBに保存する💾
- 外部API叩く🌐
- 画面に通知する🔔
こういうのって、戻り値だけ見ても「本当に実行された?」が分からないことが多いのね🙈 だから「呼び出した?」を仕様としてテストするのがこの章!
いつ“呼び出し確認”を使うの?🧭
使ってOKな場面✅(おすすめ)
- “境界”で起きる副作用(外部I/O)
例:
IEmailSender.Send(...)、ILogger.Log(...)、IRepository.Save(...)
できれば避けたい場面⚠️(壊れやすい)
-
内部の実装手順(「内部メソッドを何回呼んだ」みたいなやつ)
- それは仕様じゃなくて“実装の都合”になりがち😵💫
合言葉:**「外へ出る影響はテストしてOK、内部手順はなるべくテストしない」**💡✨
最新メモ(今の安定版の目安)🧷✨
- .NET 10 の最新パッチは **10.0.2(2026-01-13)**だよ📅 (Microsoft)
- xUnit v3 は NuGet だと xunit.v3 3.2.2 が出てるよ🧪 (nuget.org)
- モックは例として Moq 4.20.72 を使うね🎭 (nuget.org)
実践:通知の“した/しない”を仕様にする🧪📣
題材はこれにするよ👇(ちっちゃくて分かりやすい✨)
- 注文確定したら、条件次第で「購入完了メール」を送る📧
- 送ったら監査ログも残す🪵(※ここも副作用)
1) まずはインターフェース(境界)を用意🧩
csharp
public interface IEmailSender
{
Task SendPurchaseCompletedAsync(string toEmail, string subject, string body);
}
public interface IAuditLogger
{
void Log(string message);
}
2) 仕様(ルール)を決めよう📜✨
今回は分かりやすく👇
- 合計が 0円以下なら例外(入力ミス)🚫
- 合計が 1円以上ならメール送信📧&ログ🪵
- ただし、
emailが空なら例外🚫
3) テストを書く(呼び出し確認)🚦🧪
Moq版(呼ばれたことを検証)🎭✅
csharp
using Moq;
using Xunit;
public class PurchaseServiceTests
{
[Fact]
public async Task 合計が1円以上なら_購入完了メールを1回送る()
{
// Arrange
var email = new Mock<IEmailSender>();
var audit = new Mock<IAuditLogger>();
var sut = new PurchaseService(email.Object, audit.Object);
// Act
await sut.CompletePurchaseAsync(
toEmail: "alice@example.com",
totalYen: 1200);
// Assert
email.Verify(x =>
x.SendPurchaseCompletedAsync(
"alice@example.com",
It.IsAny<string>(),
It.IsAny<string>()),
Times.Once);
audit.Verify(x => x.Log(It.IsAny<string>()), Times.Once);
}
}
ここで大事なのは👇
- Times.Once:1回だけね!✅
It.IsAny<string>():本文とか件名までガチガチ固定しない(壊れやすいから)🙈
“呼ばれないこと”も仕様だよ🙅♀️✅
csharp
[Fact]
public async Task 合計が1円以上でないなら_メールは送らない()
{
var email = new Mock<IEmailSender>();
var audit = new Mock<IAuditLogger>();
var sut = new PurchaseService(email.Object, audit.Object);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
await sut.CompletePurchaseAsync("alice@example.com", totalYen: 0));
email.Verify(x => x.SendPurchaseCompletedAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()),
Times.Never);
audit.Verify(x => x.Log(It.IsAny<string>()), Times.Never);
}
「送らない」も仕様なの、超大事〜!✨ ここをテストしないと、いつの間にか“送っちゃうバグ”が混ざる😱
4) 最小実装(Greenへ)🌱✅
csharp
public class PurchaseService
{
private readonly IEmailSender _email;
private readonly IAuditLogger _audit;
public PurchaseService(IEmailSender email, IAuditLogger audit)
{
_email = email;
_audit = audit;
}
public async Task CompletePurchaseAsync(string toEmail, int totalYen)
{
if (string.IsNullOrWhiteSpace(toEmail))
throw new ArgumentException("email is required", nameof(toEmail));
if (totalYen <= 0)
throw new ArgumentOutOfRangeException(nameof(totalYen));
await _email.SendPurchaseCompletedAsync(
toEmail,
subject: "購入ありがとうございます",
body: $"合計 {totalYen} 円です");
_audit.Log($"Purchase completed: {toEmail}, {totalYen}");
}
}
もう一歩:引数も“仕様”にできる🎯✨
「誰に送った?」「合計が入ってる?」みたいな確認ね🙂
csharp
[Fact]
public async Task メール本文に_合計金額が入る()
{
var email = new Mock<IEmailSender>();
var audit = new Mock<IAuditLogger>();
var sut = new PurchaseService(email.Object, audit.Object);
await sut.CompletePurchaseAsync("alice@example.com", 1200);
email.Verify(x => x.SendPurchaseCompletedAsync(
"alice@example.com",
It.IsAny<string>(),
It.Is<string>(body => body.Contains("1200"))),
Times.Once);
}
It.Is<string>(...) が「引数の条件チェック」だよ〜🔍✨
やりすぎ防止!モック地獄を避けるコツ🧯😇
- ✅ 本当に仕様にしたい副作用だけ Verifyする
- ✅ ログ本文みたいな“微妙に変わりやすい文字列”は固定しない(必要ならキーワードだけ)🪵
- ✅ 「メール送ったか?」みたいな境界の呼び出しはテストしてOK
- ⚠️ 「内部でA→B→Cの順で呼んだ」みたいな手順テストはなるべくしない
お守りフレーズ:Verifyは“仕様の線”だけに引く🖊️✨
AIの使いどころ🤖✨(この章向き!)
AIはここが強いよ〜👇
- 「副作用になりうる観点」を列挙させる(通知/ログ/保存/イベント…)📣🪵💾🛰️
- 「呼ぶ/呼ばない」条件の抜けを探させる🔍
- Verifyの条件(回数・引数)を案出しさせる🎭
コピペ用プロンプト例👇
- 「この仕様で“呼ぶ/呼ばない副作用”を列挙して、テスト観点にして」
- 「呼び出し確認のテスト(回数・引数)を最小セットで提案して」
※ただし採用条件はいつも同じ:テストが通る+意図に一致✅😌
ミニ課題🎀📌
課題A:VIP通知を追加してみよ〜✨
- 合計が 10000円以上なら
SendVipThanksAsync(...)を呼ぶ(新しいIF追加)💌 - それ未満なら 呼ばない🙅♀️
やること👇
- Red:VIPのテストを書く🧪
- Green:最小実装
- Refactor:条件分岐が見やすい形に整理🧹✨
章末チェックリスト✅✅✅
- 「呼ぶ」だけじゃなく「呼ばない」もテストした
- Verifyしてるのは“外部への影響”だけ
- 引数は必要なところだけチェック(ガチガチ固定しない)
- テストが読めば仕様になる感じになってる📘✨
次の章(第35章)は「依存先が落ちたとき(例外・失敗)」を仕様にする回だよ💥🙂 この章の“呼ばない”テストが、そこでめっちゃ効いてくるよ〜!🚀