第72章:モック(Moq / NSubstitute)の使い方🧸✨ 〜1人でも「偽物」を作って爆速開発〜
この章では、**「DBや外部APIがなくても、テストを秒速で回せる」**ようになるための“モック”を覚えます💨 1人開発だと、確認作業がぜんぶ自分に降ってくるので、ここを押さえると一気にラクになります🥹🫶
※いまの最新ど真ん中は .NET 10 + C# 14 あたりの世代です(C# 14 が最新で .NET 10 対応) (Microsoft Learn)
1. モックってなに?🪄(超ざっくり)
テストで使う「偽物の相棒」です🤝✨ 本物(DB / メール送信 / 外部API)を使うと…
- 遅い🐢
- 不安定(ネット落ちたら終わり)📡💥
- テストが準備地獄になる😇
そこで、外側の依存を“偽物”に差し替えて、ロジックだけをサクッと検証します✅
よくある用語のイメージ👇
- Stub(スタブ):返り値だけ用意する偽物(「こう返してね」)
- Mock(モック):呼ばれ方までチェックできる偽物(「1回呼んだ?」)
- Fake(フェイク):簡易実装の偽物(メモリ内DBみたいなやつ)
この章は Mock中心でいきます💪😺

2. どこをモックするの?(迷わないルール)🧭✨
モックは基本ここだけ👇
✅ アプリの“外側”にあるもの
- DB(Repository)🗄️
- メール送信✉️
- 外部API🌐
- 時刻(DateTime)⏰ ←地味に超重要!
🚫 ドメインの中身はなるべくモックしない
- 値オブジェクトやエンティティは「本物」でテストした方が早いです🙂
- “本物なのに軽い”のがDDDの強みなので✨
3. Moq と NSubstitute、どっち使う?🤔🎀
どっちも人気です!
- Moq:Setup/Verify で明示的に書けるタイプ。超定番。NuGet上の最新版例:4.20.72 (NuGet)
- NSubstitute:英語っぽい読みやすさでサクサク書けるタイプ。最新版例:5.3.0 (NuGet)
この章では 両方の書き方を同じ題材で見せるので、好きな方を選べます🍩✨
4. お題:会員登録ユースケースをテストする👩💻✨
やりたいこと👇 「メールで会員登録する」
- すでに登録済みなら失敗(DB確認が必要)
- 新規なら登録して、歓迎メールを送る(メール送信が必要)
- 作成日時は固定したい(時刻が必要)
つまり DB / メール / 時刻をモックしたい👏
5. 実装(最小構成)🧩
5-1. Domain(メールとユーザー)
namespace MyApp.Domain;
public sealed record EmailAddress
{
public string Value { get; }
private EmailAddress(string value) => Value = value;
public static EmailAddress Create(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("メールが空です", nameof(value));
value = value.Trim();
if (!value.Contains('@'))
throw new ArgumentException("メール形式が変です", nameof(value));
return new EmailAddress(value);
}
public override string ToString() => Value;
}
public readonly record struct UserId(Guid Value);
public sealed class User
{
public UserId Id { get; }
public EmailAddress Email { get; }
public DateTimeOffset CreatedAt { get; }
public User(UserId id, EmailAddress email, DateTimeOffset createdAt)
{
Id = id;
Email = email;
CreatedAt = createdAt;
}
}
5-2. Application(依存はインターフェースで!)
using MyApp.Domain;
namespace MyApp.Application;
public interface IUserRepository
{
Task<bool> ExistsByEmailAsync(EmailAddress email, CancellationToken ct);
Task AddAsync(User user, CancellationToken ct);
}
public interface IEmailSender
{
Task SendWelcomeAsync(EmailAddress email, CancellationToken ct);
}
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
public sealed record RegisterUserResult(bool Success, string? Error, UserId? UserId)
{
public static RegisterUserResult Ok(UserId id) => new(true, null, id);
public static RegisterUserResult Fail(string error) => new(false, error, null);
}
public sealed class RegisterUserService
{
private readonly IUserRepository _users;
private readonly IEmailSender _mailer;
private readonly IClock _clock;
public RegisterUserService(IUserRepository users, IEmailSender mailer, IClock clock)
{
_users = users;
_mailer = mailer;
_clock = clock;
}
public async Task<RegisterUserResult> RegisterAsync(string email, CancellationToken ct = default)
{
EmailAddress mail;
try
{
mail = EmailAddress.Create(email);
}
catch
{
return RegisterUserResult.Fail("InvalidEmail");
}
if (await _users.ExistsByEmailAsync(mail, ct))
return RegisterUserResult.Fail("AlreadyRegistered");
var user = new User(new UserId(Guid.NewGuid()), mail, _clock.UtcNow);
await _users.AddAsync(user, ct);
await _mailer.SendWelcomeAsync(mail, ct);
return RegisterUserResult.Ok(user.Id);
}
}
6. テスト(Moq版)🐮✨
NuGetで Moq を追加して、テストで使います(最新版例:4.20.72) (NuGet)
using Moq;
using MyApp.Application;
using MyApp.Domain;
using Xunit;
public sealed class RegisterUserService_MoqTests
{
[Fact]
public async Task 既に登録済みなら_失敗して_登録もメールもされない()
{
var users = new Mock<IUserRepository>();
var mailer = new Mock<IEmailSender>();
var clock = new Mock<IClock>();
users.Setup(x => x.ExistsByEmailAsync(
It.Is<EmailAddress>(m => m.Value == "a@example.com"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
var sut = new RegisterUserService(users.Object, mailer.Object, clock.Object);
var result = await sut.RegisterAsync("a@example.com");
Assert.False(result.Success);
Assert.Equal("AlreadyRegistered", result.Error);
users.Verify(x => x.AddAsync(It.IsAny<User>(), It.IsAny<CancellationToken>()), Times.Never);
mailer.Verify(x => x.SendWelcomeAsync(It.IsAny<EmailAddress>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task 新規なら_登録されて_歓迎メールが1回送られる()
{
var users = new Mock<IUserRepository>();
var mailer = new Mock<IEmailSender>();
var clock = new Mock<IClock>();
var now = new DateTimeOffset(2025, 12, 31, 0, 0, 0, TimeSpan.Zero);
clock.SetupGet(x => x.UtcNow).Returns(now);
users.Setup(x => x.ExistsByEmailAsync(It.IsAny<EmailAddress>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var sut = new RegisterUserService(users.Object, mailer.Object, clock.Object);
var result = await sut.RegisterAsync("b@example.com");
Assert.True(result.Success);
Assert.Null(result.Error);
users.Verify(x => x.AddAsync(
It.Is<User>(u => u.Email.Value == "b@example.com" && u.CreatedAt == now),
It.IsAny<CancellationToken>()),
Times.Once);
mailer.Verify(x => x.SendWelcomeAsync(
It.Is<EmailAddress>(m => m.Value == "b@example.com"),
It.IsAny<CancellationToken>()),
Times.Once);
}
}
Moqのコツ🎯
Setup(...):こう呼ばれたらこう返してねVerify(...):本当に呼ばれた?回数は?SetupGet(...):プロパティの返り値固定(時刻モックに便利⏰)
7. テスト(NSubstitute版)🦄✨
NuGetで NSubstitute を追加(最新版例:5.3.0) (NuGet)
using NSubstitute;
using MyApp.Application;
using MyApp.Domain;
using Xunit;
public sealed class RegisterUserService_NSubstituteTests
{
[Fact]
public async Task 既に登録済みなら_失敗して_登録もメールもされない()
{
var users = Substitute.For<IUserRepository>();
var mailer = Substitute.For<IEmailSender>();
var clock = Substitute.For<IClock>();
users.ExistsByEmailAsync(
Arg.Is<EmailAddress>(m => m.Value == "a@example.com"),
Arg.Any<CancellationToken>())
.Returns(true);
var sut = new RegisterUserService(users, mailer, clock);
var result = await sut.RegisterAsync("a@example.com");
Assert.False(result.Success);
Assert.Equal("AlreadyRegistered", result.Error);
await users.DidNotReceive()
.AddAsync(Arg.Any<User>(), Arg.Any<CancellationToken>());
await mailer.DidNotReceive()
.SendWelcomeAsync(Arg.Any<EmailAddress>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task 新規なら_登録されて_歓迎メールが1回送られる()
{
var users = Substitute.For<IUserRepository>();
var mailer = Substitute.For<IEmailSender>();
var clock = Substitute.For<IClock>();
var now = new DateTimeOffset(2025, 12, 31, 0, 0, 0, TimeSpan.Zero);
clock.UtcNow.Returns(now);
users.ExistsByEmailAsync(Arg.Any<EmailAddress>(), Arg.Any<CancellationToken>())
.Returns(false);
var sut = new RegisterUserService(users, mailer, clock);
var result = await sut.RegisterAsync("b@example.com");
Assert.True(result.Success);
await users.Received(1)
.AddAsync(Arg.Is<User>(u => u.Email.Value == "b@example.com" && u.CreatedAt == now),
Arg.Any<CancellationToken>());
await mailer.Received(1)
.SendWelcomeAsync(Arg.Is<EmailAddress>(m => m.Value == "b@example.com"),
Arg.Any<CancellationToken>());
}
}
NSubstituteのコツ🎯
Substitute.For<IFoo>():偽物を作るReturns(...):返り値固定Received(1)/DidNotReceive():呼ばれた回数チェック📞✨
8. 1人開発で“爆速”になる使い方🛼💨
✅ まずテストで「安心ライン」を作る
- 新規登録は成功する🎉
- 既存登録は弾く🚫
- メールは1回だけ送る✉️
これがあるだけで、あとから改修しても怖くないです😌🫶
✅ “時刻”は絶対モックする⏰
DateTime.UtcNow を直で使うとテストが不安定になりがち…
IClock は地味だけど超効きます💊✨
9. やりがち注意⚠️(ここで詰まる子多い!)
-
❌ 内部の実装までVerifyしすぎる → テストが「仕様」じゃなくて「実装の監視カメラ」になる📹😇 → リファクタで壊れやすい
-
❌ ドメインモデルをモックする → それ、DDDの旨味が減る🥲 → 軽い本物を作ってテストした方が早いこと多い
-
✅ モックは“境界”だけ! Repository / 外部I/O / 時刻 くらいに絞ると勝ちです🏆✨
10. AIに手伝ってもらうプロンプト例🤖💗
そのままコピペOK系👇(あなた好みに変えてね)
RegisterUserService のユニットテストを xUnit で作って。
依存は IUserRepository, IEmailSender, IClock。
テストは2本:
(1) 既存メールなら AlreadyRegistered を返し、AddAsync と SendWelcomeAsync は呼ばれない
(2) 新規メールなら成功し、AddAsync と SendWelcomeAsync が1回呼ばれる。CreatedAt は IClock.UtcNow を使う
モックは Moq(または NSubstitute)で。
AIが出したコードは、必ず自分で「何を保証してるテストか」だけ確認してね☺️🔍 (テストがズレてたら、守ってくれないので…!)
今日のまとめ🌸✨
- モックは「外側(DB/メール/外部/時刻)」を偽物にして、テストを速く・安定させる🧸
- Moqは
Setup/Verify、NSubstituteはReturns/Receivedで覚える🎯 - 1人開発ほど、モックで“安心”を買うと、後半が爆速になる🏎️💨
次の章(第73章)は「設計ルールを破ったらビルドを落とす」系の話で、また違うタイプの“守り”が増えていきます🛡️✨