目次
概要
単体テストにおいて「エラー処理」や「異常系」の動作を確認することは非常に重要です。 Moqライブラリを使用すると、メソッドが呼び出された際に意図的に例外(Exception)を発生させることができます。これにより、例外発生時のアプリケーションの挙動(ログ出力、リトライ処理、エラーメッセージ表示など)を安全にテストできます。
仕様(入出力)
- 入力
- モック化するインターフェース(例:在庫管理サービス)
- 例外を発生させるトリガーとなる引数条件
- 出力
- 指定した条件でメソッドが呼ばれた際、指定した例外がスローされる。
- 前提
- NuGetパッケージ
Moqがインストールされていること。 - コンソールアプリとして動作し、
try-catchブロックで例外を捕捉して確認する。
- NuGetパッケージ
基本の使い方
.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>()を使用してください。
- 非同期メソッド(
注意点
- ジェネリック指定とインスタンス指定の違い
.Throws<ArgumentException>()はnew ArgumentException()を内部で生成して投げます(引数なしコンストラクタ)。- メッセージやパラメータ(
ParamNameなど)を含めたい場合は、必ずnewしてインスタンスを渡す形式を使用してください。
- 実行順序と重複マッチ
- 複数の
Setupが条件にマッチする場合、Moqは最後に設定された(または最も具体的な)設定を優先する傾向がありますが、混乱を避けるために条件(It.Is)は明確に分離することをお勧めします。
- 複数の
- 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(...)) を選択してください。これにより、異常系フローの網羅率を高め、堅牢なアプリケーションを構築できます。
