目次
概要
.NETのモックライブラリ「Moq」を使用して、メソッドが呼び出された際の振る舞いを定義する方法です。 特定の引数が渡された場合のみ値を返す「完全一致」、条件を満たす引数に反応する「条件マッチ」、そして渡された引数を使って計算結果を返す「動的な戻り値」の3つのパターンを解説します。
仕様(入出力)
- 入力
- モック化したいインターフェース(例: チケット料金計算)
Setup: メソッドの呼び出し定義Returns: 戻り値の定義
- 出力
- パターン1(完全一致): 引数が
0の場合のみ0を返す。それ以外はデフォルト値を返す。 - パターン2(条件マッチ): 引数が
65以上の場合に1000を返す。 - パターン3(動的算出): 引数(年齢)に応じて、子供料金や通常料金を計算して返す。
- パターン1(完全一致): 引数が
- 前提
- NuGetパッケージ
Moqが必要です。
- NuGetパッケージ
基本の使い方
特定の引数(ここでは 0)が渡されたときだけ、決まった値を返す最も基本的な設定です。
var mock = new Mock<ITicketPricing>();
// 引数が 0 のときだけ 0 を返す(完全一致)
mock.Setup(x => x.GetPrice(0)).Returns(0);
// 実行
int price = mock.Object.GetPrice(0); // 0
コード全文
「完全一致」「条件マッチ」「引数を使った動的ロジック」の3パターンを、チケット料金計算のシナリオで実装したコードです。
using System;
using Moq;
// 実行に必要なNuGetパッケージ
// dotnet add package Moq
namespace MoqMethodExample
{
// テスト対象のインターフェース:チケット料金計算
public interface ITicketPricing
{
int GetPrice(int age);
}
class Program
{
static void Main()
{
Console.WriteLine("--- 1. 特定の引数だけ固定値を返す (Exact Match) ---");
RunExactMatchDemo();
Console.WriteLine("\n--- 2. 条件に合う引数の場合に値を返す (It.Is) ---");
RunConditionalMatchDemo();
Console.WriteLine("\n--- 3. 引数を使って戻り値を計算する (Dynamic Returns) ---");
RunDynamicReturnDemo();
}
static void RunExactMatchDemo()
{
var mock = new Mock<ITicketPricing>();
// 引数が「0」の時だけ 0 を返す
// それ以外の引数で呼ばれた場合は、intのデフォルト(0)が返るが、
// Strictモードでない限り設定なしとして扱われる
mock.Setup(m => m.GetPrice(0)).Returns(0);
// 別の値も設定してみる
mock.Setup(m => m.GetPrice(20)).Returns(1800);
var calculator = mock.Object;
Console.WriteLine($"0歳: {calculator.GetPrice(0)}円"); // 0
Console.WriteLine($"20歳: {calculator.GetPrice(20)}円"); // 1800
Console.WriteLine($"99歳: {calculator.GetPrice(99)}円"); // 設定なし -> 0
}
static void RunConditionalMatchDemo()
{
var mock = new Mock<ITicketPricing>();
// 引数が「65以上」の場合に 1000 を返す
mock.Setup(m => m.GetPrice(It.Is<int>(age => age >= 65)))
.Returns(1000);
var calculator = mock.Object;
Console.WriteLine($"65歳: {calculator.GetPrice(65)}円"); // 1000
Console.WriteLine($"80歳: {calculator.GetPrice(80)}円"); // 1000
Console.WriteLine($"20歳: {calculator.GetPrice(20)}円"); // 設定なし -> 0
}
static void RunDynamicReturnDemo()
{
var mock = new Mock<ITicketPricing>();
// どんな引数(It.IsAny)が来ても、Returns内のラムダ式で計算して値を返す
mock.Setup(m => m.GetPrice(It.IsAny<int>()))
.Returns((int age) =>
{
if (age < 0) throw new ArgumentException("年齢が不正です");
if (age <= 12) return 900; // 子供料金
if (age >= 65) return 1200; // シニア料金
return 1900; // 通常料金
});
var calculator = mock.Object;
try
{
Console.WriteLine($"10歳: {calculator.GetPrice(10)}円"); // 900
Console.WriteLine($"30歳: {calculator.GetPrice(30)}円"); // 1900
Console.WriteLine($"70歳: {calculator.GetPrice(70)}円"); // 1200
// 例外の確認
calculator.GetPrice(-5);
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
}
}
}
出力例
--- 1. 特定の引数だけ固定値を返す (Exact Match) ---
0歳: 0円
20歳: 1800円
99歳: 0円
--- 2. 条件に合う引数の場合に値を返す (It.Is) ---
65歳: 1000円
80歳: 1000円
20歳: 0円
--- 3. 引数を使って戻り値を計算する (Dynamic Returns) ---
10歳: 900円
30歳: 1900円
70歳: 1200円
エラー: 年齢が不正です
カスタムポイント
- 非同期メソッドのモック化
- メソッドが
Task<int>を返す場合は、.ReturnsAsync(1000)や.ReturnsAsync((int x) => x * 10)を使用します。
- メソッドが
- 例外のスロー
.Returnsの代わりに.Throws<Exception>()または.Throws(new Exception("..."))を使うと、特定条件で例外を発生させるテストが可能です。
- コールバック
- 戻り値だけでなく、メソッド呼び出し時に変数の状態を変えたい場合などは
.Callback((int x) => ...)を併用します。
- 戻り値だけでなく、メソッド呼び出し時に変数の状態を変えたい場合などは
注意点
- It.IsAnyと型指定
It.IsAny<T>()は「その型のあらゆる値」にマッチします。特定の引数のみ例外を投げたい場合など、It.Is<T>(条件)と混在させる場合は、Setupの記述順序や条件の重複に注意してください(Moqは基本的に「後勝ち(Last definition wins)」または条件の特異性で解決しますが、明確に分けるのが安全です)。
- Strictモードの挙動
- デフォルト(Looseモード)では、Setupされていない引数で呼ばれるとデフォルト値(0やnull)を返しますが、
MockBehavior.Strictで作成している場合は例外が発生します。テストの方針に合わせて使い分けてください。
- デフォルト(Looseモード)では、Setupされていない引数で呼ばれるとデフォルト値(0やnull)を返しますが、
- 複雑すぎるロジック
Returnsの中に複雑な分岐(if/else)を書きすぎると、何をテストしているのか分からなくなります。モックはあくまで「テスト対象に都合の良い振る舞い」をさせるためのものなので、ロジックは最小限に留めてください。
応用
呼び出されるたびに異なる値を返す(シーケンス)
「1回目は成功、2回目は失敗」のようなテストには SetupSequence が役立ちます。
mock.SetupSequence(m => m.GetPrice(It.IsAny<int>()))
.Returns(1000) // 1回目
.Returns(500) // 2回目
.Throws(new Exception("System Error")); // 3回目
まとめ
Moqの Setup メソッドは、引数の条件指定(It.Is)と戻り値の動的生成(Returns((T arg) => ...))を組み合わせることで、非常に柔軟な振る舞いを定義できます。単純な値の固定には「完全一致」を、範囲指定には「条件マッチ」を、入力値の加工が必要な場合は「動的算出」を選択してください。
