辅助方法不嫌多
經過了一些《表達式樹》、《尾遞歸》等冷門內容,我們再回到一些人民群眾喜聞樂見的話題上來,繼續《最佳實踐》的討論。
在開發項目過程中,總是會出現大量的輔助方法,例如字符串處理,代碼檢驗,格式輸出等等。如果您發現自己在多次編寫類似的代碼,可能就要想著如何把這些代碼進行提取,變成輔助方法(亦或是類庫甚至框架,關于這方面粒度問題在此不作討論)。輔助方法的作用除了遵循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方法調用結果的斷言,其中包括以下幾點:
這不可或缺的五點要求總共占用了十幾行代碼(雖然它們都非常清晰明白)。如果每個單元測試方法都需要編寫這些代碼,這無疑是一件令人乏味的事情。這時,您就可以提供輔助方法來簡化單元測試的編寫。
“等一下,你說要為單元測試編寫輔助方法,這值得嗎?”的確,老趙也見過不少朋友認為,為這種“非功能性”的代碼投入太多成本是一件價值不大的事情。其實關于這一點和討論“單元測試是否有必要”是差不多的事情,如果您把單元測試視為一種可有可無的輔助品,那么的確不值得這么做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目前也提供了類似的功能。
轉載于:https://my.oschina.net/abcijkxyz/blog/721666
總結
- 上一篇: 最好的半入耳蓝牙耳机是哪些?盘点2023
- 下一篇: 总结两个平常打代码犯的“低级”错误