目次
概要
Moqを使用して、モックオブジェクトのメソッドが呼び出されたタイミングで任意のコードを実行する方法です。 単に値を返すだけでなく、呼び出された際の引数を変数に保存して後で検証したり、デバッグ用にコンソール出力を行ったりといった「副作用」を定義するために Callback メソッドを使用します。
仕様(入出力)
- 入力
- モック化するインターフェース(例:メール送信サービス)
Callback: メソッド呼び出し時に実行したいアクション(デリゲート)
- 出力
- モックのメソッドが実行されると同時に、指定したC#コードが実行される。
- 今回は引数(宛先と本文)を外部のリストに記録し、コンソールにも出力する。
- 前提
- NuGetパッケージ
Moqが必要です。
- NuGetパッケージ
基本の使い方
Setup メソッドに続けて .Callback(...) を記述します。引数の型に合わせてジェネリックを指定します。
var mock = new Mock<ILogger>();
var logs = new List<string>();
// Logメソッドが呼ばれたら、引数のメッセージをリストに追加する
mock.Setup(x => x.Log(It.IsAny<string>()))
.Callback<string>(msg => logs.Add(msg));
// 実行
mock.Object.Log("Error occurred");
コード全文
メール送信サービスのモックを作成し、送信メソッドが呼ばれた際に「送信履歴リスト」へデータを保存しつつ、戻り値としては「成功(true)」を返す完全なコードです。
using System;
using System.Collections.Generic;
using Moq;
// NuGetパッケージ "Moq" のインストールが必要です
// dotnet add package Moq
namespace MoqCallbackExample
{
// テスト対象のインターフェース:メッセージ送信機能
public interface IMessageSender
{
bool Send(string to, string body);
}
class Program
{
static void Main()
{
// 1. 検証用に送信履歴を保存するリストを用意
var sentHistory = new List<string>();
// 2. モックの作成
var mock = new Mock<IMessageSender>();
// 3. Setupで振る舞いを定義
// 引数: どんな文字列でも受け付ける (It.IsAny<string>)
mock.Setup(m => m.Send(It.IsAny<string>(), It.IsAny<string>()))
// ★ Callback: 呼び出された瞬間に実行する処理
.Callback<string, string>((to, body) =>
{
string log = $"[Callback検知] 宛先:{to}, 本文:{body}";
Console.WriteLine(log); // コンソール出力
sentHistory.Add(log); // リストへ保存
})
// ★ Returns: メソッドの戻り値(成功したことにする)
.Returns(true);
var sender = mock.Object;
Console.WriteLine("--- 実行開始 ---");
// 4. モックメソッドの呼び出し
bool result1 = sender.Send("user@example.com", "こんにちは");
bool result2 = sender.Send("admin@example.com", "システム通知");
Console.WriteLine($"\n戻り値確認: 1回目={result1}, 2回目={result2}");
Console.WriteLine("\n--- 履歴リストの確認 ---");
foreach (var log in sentHistory)
{
Console.WriteLine(log);
}
}
}
}
出力例
--- 実行開始 ---
[Callback検知] 宛先:user@example.com, 本文:こんにちは
[Callback検知] 宛先:admin@example.com, 本文:システム通知
戻り値確認: 1回目=True, 2回目=True
--- 履歴リストの確認 ---
[Callback検知] 宛先:user@example.com, 本文:こんにちは
[Callback検知] 宛先:admin@example.com, 本文:システム通知
カスタムポイント
- 引数の数に合わせる
- 引数が1つの場合は
.Callback<int>(i => ...) - 引数が2つの場合は
.Callback<string, int>((s, i) => ...) - 引数がない場合は
.Callback(() => ...) - 最大16個の引数まで対応しています。
- 引数が1つの場合は
- 例外テストへの利用
- 条件によって
Returnsでは表現しきれない複雑な状態変更や、テスト用フラグの操作などを記述できます。
- 条件によって
注意点
- CallbackとReturnsの順序
- 通常は
Callback(...).Returns(...)の順で記述します。メソッドチェーンとしてつなげることで、処理を実行した上で値を返すことができます。
- 通常は
- Verifyとの使い分け
- 単に「メソッドが特定の引数で呼ばれたか」を確認するだけであれば、Callbackを使わずに
mock.Verify(m => m.Send("...", "..."), Times.Once())を使う方が標準的でスマートです。Callbackは「引数の値をキャプチャして、その中身を詳細に検査したい」場合などに有効です。
- 単に「メソッドが特定の引数で呼ばれたか」を確認するだけであれば、Callbackを使わずに
- 例外処理
- Callback内で例外が発生すると、テスト実行自体が失敗(クラッシュ)する原因になります。Callback内のロジックはシンプルに保ってください。
応用
呼び出しの前後で処理を挟む(Before/After)
厳密なAOP(アスペクト指向)のような Before/After はありませんが、Callback を複数記述することで似た挙動を実現できます。
mock.Setup(m => m.Process())
.Callback(() => Console.WriteLine("処理開始前"))
.Returns(() =>
{
Console.WriteLine("処理中(戻り値計算)");
return 100;
})
.Callback(() => Console.WriteLine("処理完了後(※Returnsの後には書けないため工夫が必要)"));
※ Returns の後に Callback は書けません。メソッド終了後の処理が必要な場合は、Returns のデリゲート内で処理してから値を返すように実装します。
まとめ
Moqの Callback を使用すると、モックメソッドが呼び出された際の引数をキャプチャしたり、外部の状態を変更したりといった柔軟な処理が可能になります。Verify だけでは検証しきれない複雑なオブジェクトの受け渡しを確認する際や、デバッグ時のログ出力において非常に強力なツールとなります。
