【C#】Moqでメソッドの引数条件と動的な戻り値を定義する方法

目次

概要

.NETのモックライブラリ「Moq」を使用して、メソッドが呼び出された際の振る舞いを定義する方法です。 特定の引数が渡された場合のみ値を返す「完全一致」、条件を満たす引数に反応する「条件マッチ」、そして渡された引数を使って計算結果を返す「動的な戻り値」の3つのパターンを解説します。

仕様(入出力)

  • 入力
    • モック化したいインターフェース(例: チケット料金計算)
    • Setup: メソッドの呼び出し定義
    • Returns: 戻り値の定義
  • 出力
    • パターン1(完全一致): 引数が 0 の場合のみ 0 を返す。それ以外はデフォルト値を返す。
    • パターン2(条件マッチ): 引数が 65 以上の場合に 1000 を返す。
    • パターン3(動的算出): 引数(年齢)に応じて、子供料金や通常料金を計算して返す。
  • 前提
    • NuGetパッケージ Moq が必要です。

基本の使い方

特定の引数(ここでは 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) => ...) を併用します。

注意点

  1. It.IsAnyと型指定
    • It.IsAny<T>() は「その型のあらゆる値」にマッチします。特定の引数のみ例外を投げたい場合など、It.Is<T>(条件) と混在させる場合は、Setup の記述順序や条件の重複に注意してください(Moqは基本的に「後勝ち(Last definition wins)」または条件の特異性で解決しますが、明確に分けるのが安全です)。
  2. Strictモードの挙動
    • デフォルト(Looseモード)では、Setupされていない引数で呼ばれるとデフォルト値(0やnull)を返しますが、MockBehavior.Strict で作成している場合は例外が発生します。テストの方針に合わせて使い分けてください。
  3. 複雑すぎるロジック
    • 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) => ...))を組み合わせることで、非常に柔軟な振る舞いを定義できます。単純な値の固定には「完全一致」を、範囲指定には「条件マッチ」を、入力値の加工が必要な場合は「動的算出」を選択してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次