隨著 .NET 生態中 Mock 技術普及,Moq 憑藉 Lambda 表達式設定行為期望、捨棄傳統 Record/Replay 模式、基於 Castle DynamicProxy 實現攔截,成為主流選擇。但當被 Mock 方法包含 ref 參數時,Callback 設定卻暗藏陷阱。
public interface ITestInterface
{
string TestMethodWithRef(ref string refStr, string str);
}
[TestMethod]
public void Ref_Param_Test()
{
var mock = new Mock();
string refStr = "1";
string str = "2";
// 錯誤:Callback 參數不匹配
mock.Setup(m => m.TestMethodWithRef(ref refStr, str))
.Callback((string rs, string s) => Console.WriteLine(rs + s));
mock.Object.TestMethodWithRef(ref refStr, str);
mock.VerifyAll();
}
執行拋出:System.ArgumentException: Invalid callback. Setup on method with parameters (String&, String) cannot invoke callback with parameters (String, String)
// 編譯錯誤:Lambda 參數不能使用 ref/out
mock.Setup(m => m.TestMethodWithRef(ref refStr, str))
.Callback((ref string rs, string s) => Console.WriteLine(rs + s));
編譯器錯誤:Variables introduced within a lambda expression are not visible in the outer method
直接實作介面,完全控制行為。缺點:樣板代碼多,維護成本高。
mock.Setup(m => m.TestMethodWithRef(ref refStr, str))
.Callback(() => Console.WriteLine(refStr + str))
.Returns("")
.Verifiable();
透過閉包直接存取外部變數 refStr、str,繞過參數匹配限制。
[TestMethod]
public void Ref_Param_Test()
{
var mock = new Mock();
string refStr = "1";
string str = "1";
mock.Setup(m => m.TestMethodWithRef(ref refStr, str))
.Callback(() => { refStr = "2"; str = "2"; })
.Returns("")
.Verifiable();
mock.Object.TestMethodWithRef(ref refStr, str);
mock.VerifyAll();
Assert.AreEqual("2", str); // 通過
Assert.AreEqual("2", refStr); // 失敗!refStr 仍為 "1"
}
Moq 內部機制:Setup 時捕獲的 refStr 是當時的值副本,而非參考。Callback 中修改的是閉包捕獲的區域變數副本,不影響外部原始變數。這是 C# 閉包與 ref 語義交互的特殊行為。
out 參數模式Callback 搭配自訂類別封裝參數,或在測試後直接斷言物件狀態此問題揭示了 Mock 框架在處理值型別語義與參考語義邊界時的限制,理解底層機制有助於設計更可測試的介面。