文章目錄
使用 Moq 來 Mock protected Method
跟同事討論到在進行單元測試時,目標方法使用到其他非 public 方法,而且想要 mock 這個方法的回傳值該怎麼做?
其實這個問題我之前也思考過,一直沒有找到很好的辦法,只印象中 Moq 可以 mock protected method 的回傳,但沒有實作過,剛好趁這個機會紀錄一下
基本設定
Restful Web Api 專案,Get 方法需要傳入一個 string 參數,如果未傳入 string 或傳入的參數是空字串就回傳 BadRequestResult
,傳入參數是正常 string 就回傳 OkResult
, 其中檢查 string 的邏輯與動作就獨立包裝在 detectStringEmpty
function 中,程式碼內容如下
public class ValuesController : ApiController
{
public IHttpActionResult Get(string value)
{
var isNullorEmpty= detectStringEmpty(value);
if (isNullorEmpty)
return BadRequest();
else
return Ok();
}
private bool detectStringEmpty(string value)
{
if (string.IsNullOrWhiteSpace(value))
return true;
else
return false;
}
}
修改目標程式碼
因為 Moq 只能 mock protected method,所以要將 private method 修改為 protected
將 private 改為 protected
protected bool detectStringEmpty(string value) { if (string.IsNullOrWhiteSpace(value)) return true; else return false; }
加上 virtual
Moq 只能 mock
protected virtual
method未加
virtual
的錯誤訊息訊息內容
Test Name: Get_StringEmpty_BadRequest Test FullName: UnitTestStaticField.Controllers.Tests.ValuesControllerTests.Get_StringEmpty_BadRequest Test Source: C:\Users\YowkoTsai\documents\visual studio 2017\Projects\UnitTestStaticField\UnitTestStaticFieldMTests\Controllers\ValuesControllerTests.cs : line 71 Test Outcome: Failed Test Duration: 0:00:00.0531624 Result StackTrace: at Moq.Mock.ThrowIfCantOverride(Expression setup, MethodInfo method) at Moq.Mock.<>c__DisplayClass65_0`2.<Setup>b__0() at Moq.PexProtector.Invoke[T](Func`1 function) at Moq.Mock.Setup[T,TResult](Mock`1 mock, Expression`1 expression, Condition condition) at Moq.Protected.ProtectedMock`1.Setup[TResult](String methodName, Boolean exactParameterMatch, Object[] args) at Moq.Protected.ProtectedMock`1.Setup[TResult](String methodName, Object[] args) at UnitTestStaticField.Controllers.Tests.ValuesControllerTests.Get_StringEmpty_BadRequest() in C:\Users\YowkoTsai\documents\visual studio 2017\Projects\UnitTestStaticField\UnitTestStaticFieldMTests\Controllers\ValuesControllerTests.cs:line 79 Result Message: Test method UnitTestStaticField.Controllers.Tests.ValuesControllerTests.Get_StringEmpty_BadRequest threw exception: System.NotSupportedException: Invalid setup on a non-virtual (overridable in VB) member: mock => mock.detectStringEmpty(It.IsAny<String>())
錯誤截圖
修改後程式碼
protected virtual bool detectStringEmpty(string value) { if (string.IsNullOrWhiteSpace(value)) return true; else return false; }
撰寫測試程式
- 專案使用 NuGet 加入 Moq
引用 namespace
using Moq; using Moq.Protected;
arrange
使用 Moq 語法 mock 目標程式
var target = new Mock<ValuesController>() { CallBase = true };
mock protected virtual 方法
有兩種寫法
寫法一
var target = new Mock<ValuesController>(); target.Protected() .Setup<bool>("detectStringEmpty", new object[] { string.Empty }) .Returns(true);
寫法二
var target = new Mock<ValuesController>(); target.Protected() .Setup<bool>("detectStringEmpty",ItExpr.IsAny<string>()) .Returns(true);
定義預期結果
var expected = typeof(BadRequestResult);
act
使用 Moq 語法來執行測試目標程式方法
var actualAction = target.Object.Get(string.Empty);
這邊要特別注意,如果前面 mock 測試目標時如果沒有傳入
{ CallBase = true }
,在執行測試目標程式方法會永遠回傳 null 而造成測試結果與預期有落差將實際執行結果轉型為預期的
BadRequestResult
var actual = actualAction as BadRequestResult;
assert
檢查執行結果不能是 null
Assert.IsNotNull(actual);
驗證執行結果是否與預期相符
Assert.IsInstanceOfType(actual, expected);
完整程式碼
[TestMethod()] public void Get_StringEmpty_BadRequest() { //arrange var target = new Mock<ValuesController>() { CallBase = true }; target.Protected() .Setup<bool>("detectStringEmpty", ItExpr.IsAny<string>()) .Returns(true); var expected = typeof(BadRequestResult); //act var actualAction = target.Object.Get(string.Empty); var actual = actualAction as BadRequestResult; //assert Assert.IsNotNull(actual); Assert.IsInstanceOfType(actual, expected); }
心得
原本以為 mock framework 用法大同小異,實際嘗試後才發現各個 mock framework 語法與思維差距很大,使用上也不少眉眉角角需要留意,功能當然也各有優缺點,似乎還沒有可以符合各種情境的 framework 來降低使用門檻,聽說付費的 mock framework 功能異常強大,或許該找個時間來測試看看
回到今天的主題:測試目標程式本身只需要將 private 改為 protected virtual 就可以順利達成測試目的,雖然這樣的修改會讓 method 本身對外部程式的可見度提高,但用這點修改換取程式的可測試性應該是可以被接受的,只是如果有辦法完全不修改測試目標程式就完成測試更加完美了,假設有幸找到其他方法,我再來分享
參考資訊
文章作者 Yowko Tsai
上次更新 2021-11-02
授權合約
本部落格 (Yowko's Notes) 所有的文章內容(包含圖片),任何轉載行為,必須通知並獲本部落格作者 (Yowko Tsai) 的同意始得轉載,且轉載皆須註明出處與作者。
Yowko's Notes 由 Yowko Tsai 製作,以創用CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款 釋出。