專案開發過程中,字串處理、代碼檢驗、格式輸出等重複邏輯層出不窮。當發現自己反覆撰寫相似代碼時,便該考慮將其提取為輔助方法(或類庫、框架)。除了遵循 DRY(Don't Repeat Yourself)原則,輔助方法更能讓代碼更易撰寫、結構更清晰、可讀性更佳——且只要動手去做,收益立竿見影。
以 ASP.NET MVC HomeController.Index 的單元測試為例,驗證回傳 ViewResult、預設視圖/母版頁名稱、Model 型別與屬性值等五項條件,原始代碼超過十幾行:
[TestMethod]
public void IndexTest()
{
UserIdentity identity = new UserIdentity();
Mock<HomeController> mockController = new Mock<HomeController>() { CallBase = true };
mockController.Setup(c => c.Identity).Returns(identity);
var result = mockController.Object.Index() as ViewResult;
if (result == null)
throw new Exception("result is expected to be ViewResult but not.");
Assert.AreEqual("", result.ViewName, "the view name is expected to be the default one but '{0}'", result.ViewName);
Assert.AreEqual("", result.MasterName, "the master name is expected to be the default one but '{0}'", result.MasterName);
var model = result.ViewData.Model as IndexModel;
if (model == null)
throw new Exception("model is expected to be IndexModel but not.");
Assert.AreEqual(identity, model.Identity);
Assert.AreEqual("Welcome to ASP.NET MVC!", model.Message);
}
若每個測試方法都重複這段邏輯,極其乏味且難以維護。
定義擴展方法 Is<T> 與 IsView 封裝斷言邏輯,測試瞬間精簡為:
[TestMethod]
public void IndexTest()
{
UserIdentity identity = new UserIdentity();
Mock<HomeController> mockController = new Mock<HomeController>() { CallBase = true };
mockController.Setup(c => c.Identity).Returns(identity);
var result = mockController.Object.Index().Is<ViewResult>().IsView(null, null);
var model = result.ViewData.Model.Is<IndexModel>();
Assert.AreEqual(identity, model.Identity);
Assert.AreEqual("Welcome to ASP.NET MVC!", model.Message);
}
代碼行數大幅減少,且幾乎全靠 IntelliSense 提示完成,編寫體驗順暢。
public static class AssertHelpers
{
public static T Is<T>(this object result)
{
Assert.IsTrue(
result is T,
"actionResult is expected to be '{0}' but '{1}'", typeof(T), result.GetType());
return (T)result;
}
public static T IsView<T>(this T result, string viewName, string masterName) where T : ViewResult
{
viewName = viewName ?? "";
masterName = masterName ?? "";
Assert.IsTrue(
String.Equals(viewName, result.ViewName, StringComparison.InvariantCultureIgnoreCase),
"The view name is expected to be {0} but {1}",
viewName == "" ? "the default one" : "'" + viewName + "'",
result.ViewName == "" ? "the default one" : "'" + result.ViewName + "'");
Assert.IsTrue(
String.Equals(masterName, result.MasterName, StringComparison.InvariantCultureIgnoreCase),
"The master name is expected to be {0} but {1}",
masterName == "" ? "the default one" : "'" + masterName + "'",
result.MasterName == "" ? "the default one" : "'" + result.MasterName + "'");
return result;
}
}
此處運用 C# 3.0 擴展方法 特性,無侵入性地為既有型別添加行為。
Enum 基類擴展,讓原本無法包含成員的列舉擁有方法。測試代碼透過 Is...IsView 鏈式呼叫模擬自然語言語法,大幅提升可讀性。良好的 API 設計能顯著提高開發效率,正如 Rhino Mocks 以強型別方法呼叫取代 NMock 的字串傳遞,重構時編譯器即可捕捉錯誤,避免運行時才發現測試失效。
微軟產品以「易用性」著稱,.NET 平台在語言特性與 API 設計上總體令人滿意。《Framework Design Guidelines》一書(第 16 屆 Jolt 大獎得主)沉澱了這些設計準則,值得研讀。
若視單元測試為專案核心資產,投資輔助方法絕非浪費。它不僅減少樣板代碼,更能推動團隊編寫更具表達力、可維護性更高的測試——這正是專案長期健康發展的關鍵。