【C#】Moqでメソッド呼び出し時に例外を発生させる方法

目次

概要

単体テストにおいて「エラー処理」や「異常系」の動作を確認することは非常に重要です。 Moqライブラリを使用すると、メソッドが呼び出された際に意図的に例外(Exception)を発生させることができます。これにより、例外発生時のアプリケーションの挙動(ログ出力、リトライ処理、エラーメッセージ表示など)を安全にテストできます。

仕様(入出力)

  • 入力
    • モック化するインターフェース(例:在庫管理サービス)
    • 例外を発生させるトリガーとなる引数条件
  • 出力
    • 指定した条件でメソッドが呼ばれた際、指定した例外がスローされる。
  • 前提
    • NuGetパッケージ Moq がインストールされていること。
    • コンソールアプリとして動作し、try-catch ブロックで例外を捕捉して確認する。

基本の使い方

.Throws<TException>() を使用して、指定した型の例外をスローさせます。

var mock = new Mock<IPaymentGateway>();

// 引数が null の場合に ArgumentNullException を投げる
mock.Setup(x => x.ProcessPayment(null))
    .Throws<ArgumentNullException>();

コード全文

「在庫管理サービス」を模したシナリオです。 引数の条件(無効なID、不正な数量)に応じて、異なる例外を発生させる完全なコード例です。

using System;
using Moq;

// NuGetパッケージ "Moq" が必要です
// dotnet add package Moq

namespace MoqExceptionExample
{
    // テスト対象のインターフェース:在庫サービス
    public interface IInventoryService
    {
        // 指定した商品の在庫を引き落とす
        void RemoveStock(int productId, int quantity);
    }

    class Program
    {
        static void Main()
        {
            // モックの作成
            var mock = new Mock<IInventoryService>();

            // 設定1: 数量(quantity)が 0以下の時、ArgumentOutOfRangeException を投げる(型のみ指定)
            mock.Setup(s => s.RemoveStock(It.IsAny<int>(), It.Is<int>(q => q <= 0)))
                .Throws<ArgumentOutOfRangeException>();

            // 設定2: 商品ID(productId)が 999 の時、具体的なメッセージ付きの例外を投げる(インスタンス指定)
            mock.Setup(s => s.RemoveStock(999, It.IsAny<int>()))
                .Throws(new InvalidOperationException("商品ID:999 は現在取り扱い停止中です。"));

            // 設定3: 正常系(それ以外は成功とみなす設定は省略可だが、明示的に何もしないことを示す)
            mock.Setup(s => s.RemoveStock(1, 10)).Verifiable();

            var service = mock.Object;

            Console.WriteLine("--- テスト開始 ---");

            // ケース1: 不正な数量による例外テスト
            try
            {
                Console.WriteLine("1. 数量 -5 で引き落としを試行...");
                service.RemoveStock(1, -5);
            }
            catch (ArgumentOutOfRangeException)
            {
                Console.WriteLine("-> 成功: 想定通り ArgumentOutOfRangeException が発生しました。");
            }

            // ケース2: 特定IDによる例外テスト
            try
            {
                Console.WriteLine("\n2. 禁止ID(999) で引き落としを試行...");
                service.RemoveStock(999, 10);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine($"-> 成功: 想定通りの例外が発生しました。Msg: {ex.Message}");
            }

            // ケース3: 正常な呼び出し
            try
            {
                Console.WriteLine("\n3. 正常なIDと数量で試行...");
                service.RemoveStock(1, 10);
                Console.WriteLine("-> 成功: 例外は発生しませんでした。");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"-> 失敗: 予期せぬ例外が発生しました: {ex}");
            }
        }
    }
}

出力例

--- テスト開始 ---
1. 数量 -5 で引き落としを試行...
-> 成功: 想定通り ArgumentOutOfRangeException が発生しました。

2. 禁止ID(999) で引き落としを試行...
-> 成功: 想定通りの例外が発生しました。Msg: 商品ID:999 は現在取り扱い停止中です。

3. 正常なIDと数量で試行...
-> 成功: 例外は発生しませんでした。

カスタムポイント

  • 例外メッセージの検証が必要な場合
    • Throws<Exception>() ではなく、Throws(new Exception("メッセージ")) のようにインスタンスを渡すことで、例外メッセージの内容まで含めた厳密なテストが可能になります。
  • 非同期メソッドの場合
    • 非同期メソッド(Taskを返すメソッド)の場合は ThrowsAsync<Exception>() を使用してください。

注意点

  1. ジェネリック指定とインスタンス指定の違い
    • .Throws<ArgumentException>()new ArgumentException() を内部で生成して投げます(引数なしコンストラクタ)。
    • メッセージやパラメータ(ParamNameなど)を含めたい場合は、必ず new してインスタンスを渡す形式を使用してください。
  2. 実行順序と重複マッチ
    • 複数の Setup が条件にマッチする場合、Moqは最後に設定された(または最も具体的な)設定を優先する傾向がありますが、混乱を避けるために条件(It.Is)は明確に分離することをお勧めします。
  3. Strictモードの挙動
    • MockBehavior.Strict を使用している場合、Setup していない引数でメソッドを呼ぶと、ここで設定した例外とは別に MockException が発生します。意図した例外テストの妨げにならないよう注意してください。

応用

リトライ処理のテスト(最初は失敗し、次は成功する)

「一時的なネットワークエラー」などのリトライロジックをテストするために、呼び出されるたびに挙動を変える SetupSequence が役立ちます。

// 1回目は例外、2回目は正常終了
mock.SetupSequence(s => s.RemoveStock(It.IsAny<int>(), It.IsAny<int>()))
    .Throws(new TimeoutException("通信タイムアウト"))
    .Pass(); // Pass() は voidメソッドの場合に何もしない(成功)を表す

まとめ

Moqで例外を発生させるには Throws メソッドを使用します。単純な例外型チェックならジェネリック版の Throws<T>() を、エラーメッセージやプロパティの検証が必要ならインスタンス版の Throws(new T(...)) を選択してください。これにより、異常系フローの網羅率を高め、堅牢なアプリケーションを構築できます。

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

この記事を書いた人

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

目次