在開發項目過程中,總是會出現大量的輔助方法,例如字符串處理,代碼檢 驗,格式輸出等等。如果您發現自己在多次編寫類似的代碼,可能就要想著如何 把這些代碼進行提取,變成輔助方法(亦或是類庫甚至框架,關於這方面粒度問 題在此不作討論)。輔助方法的作用除了遵循DRY原則之外,也能讓代碼更容易 編寫,更為清晰,可讀性也能更好——而且只要您“去做”,就會發現要得到這 些好處並不困難。
在這裡舉一個最簡單的例子,對Index方法的單元測試:
[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);
}
從“var result = ...”這一行代碼開始到結尾,都是對Index方法調用結果 的斷言,其中包括以下幾點:
返回值為ViewResult對象
ViewName是默認值
MasterName是默認字符串
Model為IndexModel對象
Model的各屬性為正確的值
這不可或缺的五點要求總共占用了十幾行代碼(雖然它們都非常清晰明白) 。如果每個單元測試方法都需要編寫這些代碼,這無疑是一件令人乏味的事情。 這時,您就可以提供輔助方法來簡化單元測試的編寫。
“等一下,你說要為單元測試編寫輔助方法,這值得嗎?”的確,老趙也見 過不少朋友認為,為這種“非功能性”的代碼投入太多成本是一件價值不大的事 情。其實關於這一點和討論“單元測試是否有必要”是差不多的事情,如果您把 單元測試視為一種可有可無的輔助品,那麼的確不值得這麼做1。如 果您認為單元測試是項目的一部分,那麼讓這部分代碼更容易編寫又有何不可呢 ?更何況……您不妨先看一下使用輔助方法之後這部分代碼的模樣:
[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);
}
不知道您的感受如何,不過這些代碼當時的確讓老趙欣喜了一把。長篇冗繁 的判斷代碼變成寥寥數行,而且如果您也可以想象一下在編寫這些代碼時的感覺 ——幾乎都由IDE提示完成。而且,編寫這些輔助方法其實非常容易:
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這個所有枚舉類型的基 類添加擴展方法,這樣可以使原本無法包含其它成員的枚舉類型似乎也有了方法 。這個示例提供了一個擴展方法,能夠從每個枚舉類型中獲取附加的數據。
針對接口類型定義擴展方法,這樣所有實現這個接口的類型都會獲得額外的 方法——是不是有種獲得“多繼承”特性的感覺?同樣是這個示例,針對 ICustomAttributeProvider定義擴展方法,為Type,MethodInfo,ProperyInfo 等類型同時添加了擴展。
把原本定義在某些基類才能讓所有子類訪問到的方法,轉移成擴展方法,這 樣降低了代碼之間耦合性。當然,這樣的修改需要您重新編譯(但不需要修改) 代碼。這個示例通過針對Control類型的擴展,為所有的控件、頁面和模板頁添 加了FastEval擴展方法。
此外,測試代碼的可讀性也提高了一個級別,我們使用了Is…IsView等方法 “模擬”了自然的英語語法。在Java和C#等語言中實現這種自然的文法並不是一 件簡單的事情(相對於Ruby,F#等語言來說)。不過我們也可以朝這個方向去努 力一把,而最後的結果似乎也令人較為滿意。
在這裡還有個題外話:如今API的優劣已經大大影響一個語言、平台、框架在 開發群體中的地位。開發人員往往會因為“順手”這個看似“無理的理由”改變 自己對於某個框架、平台或者語言的選擇——其實原因也很容易理解,因為良好 的優秀的API設計能夠大大提高開發效率。這是個不爭的事實,我們有時會說某 某語言“它就是在寫英文啊”(例如傳說中的AppleScript),其實就是再指這 門語言在描述程序的“語義”時與真實語法特別接近。舉個更貼近.NET的例子, 使用NMock,RhinoMocks這兩個.NET單元測試領域中大名鼎鼎的Mock框架對一個 方法調用作期望(Expect)時,就可以看出它們在API設計上就有很大的不同:
interface ICalculator
{
int Sum(int a, int b);
}
class TestFixture
{
void TestByNMock()
{
var mocks = new Mockery();
var mockCalculator = mock.NewMock<ICalculator> ();
Expect.Once.On(mockCalculator)
.Method("Sum")
.With(1, 2)
.Will(Return.Value(3));
// use the mockCalculator object...
mocks.VerifyAllExpectationsHaveBeenMet();
}
void TestByRhinoMocks()
{
var mocks = new MockRepository();
var mockCalculator = mocks.CreateMock<ICalculator>();
Expect.Call(mockCalculator.Sum(1, 2)).Return(3);
mocks.ReplayAll();
// use the mockCalculator object...
mocks.VerifyAll();
}
}
作為流行的Mock框架,無論是NMock的Expect...On...Method...With...Will Return式語法,或者RhinoMocks的Expect.Call...Return式語法在編程的“語義 ”方面都做得不錯——不過Rhino Mocks明顯更勝一步2。其原因就 在於RhinoMocks使用了顯式的方法調用和參數傳遞替代了NMock的字符串傳遞語 法。這個優勢使得開發人員在編寫單元測試時可以在編機器中得到良好的代碼提 示,在重構時也可以讓編輯器同時修改Mock對象的方法名,至少也可以讓編譯器 提示錯誤。反之,如果使用字符串,則在Mock方法名修改之後還必須在運行時才 能發現問題。一個簡單重構就會破壞數個甚至更多的單元測試,這無疑是一個令 人沮喪的現象。
作為一個從VB 5/6(2年)轉向Delphi(1年),後又轉向Java(1年半),最 後立足於.NET平台,同時也在不斷地關注著各類語言/平台發展的開發人員,我 的看法應該不是井蛙之見。微軟的產品以“易用性”著稱,這一點在其開發領域 也得到了繼承。在對語言特性和API設計這方面,.NET平台總體來說讓我非常滿 意。例如在.NET裡使用C# 3.0的特性進行開發經常讓我有一種愉快的感覺。.NET 框架在其大部分類庫中也提供了非常方便、直觀的API設計,在編輯器的代碼提 示幫助下,一個有經驗的開發人員甚至可以擺脫文檔來寫出一段能夠“解決問題 ”的程序來。而微軟在.NET框架中提煉出來的設計准則也被寫入了《Framework Design Guidelines》一書中,它是第16屆年度Jolt大獎的圖書,現在其第二版 也已經上市。我想您應該不會錯過這些。
注1:如果您覺得單元測試可有可無,那麼可能ASP.NET MVC並不適合您,您 不妨繼續使用更容易掌握的ASP.NET WebForms框架。
注2:Moq利用了Lambda表達式在語義方法又比RhinoMocks更勝一籌,不過現 在RhinoMocks目前也提供了類似的功能。