本篇來談談Windows phone Unit Test.
原來在9月份一次線下技術沙龍現場交流.我在現場提到關於Windows phone Unit Test在實際編程所體現一些問題.可惜當時在現場回應人的太少.通過本篇將詳細梳理關於在Windows phone 開發流程做UT可能遇到的問題,以及一些具體解決方案.
關於UT.不會在這里拿太多篇幅解釋它基本的用法.當然也更不會拿時間去強調UT它在實際編程中保證軟件質量重要性.從自身角度來說.一個程序員良好的職業素養往往源自於對自身高要求,並能持之以恆的保持下去.在實際開發流程照成很多”不愉快“的體驗,其實很多從自身角度來說完全可以避免的.
其實很多Team在實際開發中拒絕寫UT.而且還不在少數.依然還是很多開發人員認為自己只是不斷貢獻產品的Code.而和UT無關.還是有那么多Program Manager太過於專注開發進度.在每次CodeReview后.忽略了為UT留下相應的時間.而在后期集成測試階段讓開發人員陷入Bug突顯修修補補“災難”之中難以自拔. 顯然實際開發中突顯種種問題.是對理想狀態下軟件工程必要流程斷章取義.而IDE開發工具越來越強大編譯能力似乎讓開發人員產生依賴.編譯通過只是說明語法正確.而無法真實確認實際Code語義是否也是願景一致.而在具有一定規模存在多分枝項目結構中.如果沒有一個完整保證軟件質量的體系和具體措施方法.很難想象這樣集成項目中對開發人員該是一種什么樣的災難.!?
well.談到Windows phone應用或是客戶端.往往實際開發規模相對於Pc Application較小. 特別是未來突出雲平台發展方向.必然會照成客戶端APP越來越瘦的趨勢.但必要測試依然是構造可靠應用程序必經之路.
<1>構建測試環境
針對Windows phone應用程序Unit Test 官方並沒有在IDE提供對應的測試框架.經過實際開發反復驗證.依然可以通過如下幾種方式建立.Windows phone 單元測試.:
建立單元測試:
[1]通過帶有官方背景的Jeff Wilcox’s 更新Silverlight Unit Test Framework的Windows phone版本建立單元測試.具體請參見Updated Silverlight Unit Test Framework bits for Windows Phone and Silverlight 3 And Unit Testing Silverlight & Windows Phone Applications
[2]通過第三方測試框架Windows phone Test FrameWork或NUnit For Windows phone 等構建
在目前Windows phone應用程序中建立單元測試框架中在開發者群體使用最廣泛的是Jeff Wilcox’s 維護更新的Silverlight Unit TEst FrameWork For Windows phone版本.其實熟悉Silverlight開發的同學應該知道.Jeff Wilcox是Silverlight 2版本時官方推出Unti Test FrameWork單元測試框架的主要開發人員之一.做過Silverlight單元測試的開發人員肯定知道他曾在博客寫的Silverlight 2版本單元測試系列.
在Windows phone應用程序中引用單元測試需要添加如下引用DLL:
1: //添加引用DLL
2: Microsoft.Silverlight.Testing
3: Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight
可以通過兩種方式獲得該DLL引用.方式一在Jeff Wilcox’s 博客下載 解壓即可:
[ZIP, 518K] Silverlight Unit Test Framework Assemblies compatible with Mango Beta Tools
下載完成后.解壓能看到如上兩個必須的DLL.這時解決方案添加一個普通的Windows phone Application項目.[UT測試結果需要在UI輸出].手動添加如上兩個DLL引用關系.這時會提示:
提示引用Silverlight類庫.可以不理會提示直接點擊是確定引用.
方式二: 打開Visual Studio 2011 找到Tool->Extension Manager .在Online Gallery選項中搜索:Windows phone Test Project 可以看到:
可以直接通過點擊Download下載安裝該項目模板.安裝完成后新建Project就能看到 Test選項頁下多了一個Windows phone Test Project模板:
新建一個測試項目命名Test project1[測試用].在執行編譯前需要安裝Nuget然后通過Tool->Library Package Manager->Package Manager Console窗口輸入如下命令添加引用:
這時會在TestProject 1實際上會看到添加三個引用:
1: //添加引用DLL
2: Microsoft.Silverlight.Testing
3: Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight
4: SilverlightSerializer.WP7
注意.當建立玩這個模板項目TestProject1會提示通過Nuget工具執行如下命令行: Install-Package Silverlight.UnitTest 和 Install-Package WindowsPhoneEssentials.Testing執行兩個指令 .在執行命令前前者因WP7SDK 更新的原因,前者並不支持Mango版本所以就不推薦使用了.一律使用后者命令初始化引用庫.
構建好測試項目后.首先在Windows phone Unit TEst中.我們既可以采用極限編程XP提倡的[TDD]Test Driver Development測試驅動的方式從上而下進行.也可以僅僅只是回顧性的編寫單元測試一遍驗證代碼的執行是否與預期的執行效果相同.本篇主要采用Silverlight Unit TEst FrameWork來構建執行WP7單元測試.
Well.在構建單元測試用例[Test Case]前.需要構建一個具有完成功能的Windows phone應用程序.這里為了演示的目的.當前應用程序以MVVM的方式實現UI上一個分類列表的顯示.首先定義一個ViewModel-MainPage_ViewModel 內容:
1: public class MainPage_ViewModel:BasicViewModel
2: {
3: ObservableCollection<CatalogInfo> catalogInfoCol = new ObservableCollection<CatalogInfo>();
4: public ObservableCollection<CatalogInfo> CatalogInfoCol
5: {
6: get { return this.catalogInfoCol; }
7: set
8: {
9: this.catalogInfoCol = value;
10: base.NotifyPropertyChangedEventHandler("CatalogInfoCol");
11: }
12: }
13:
14: public void LoadCatalogDefaultData()
15: {
16: this.catalogInfoCol.Clear();
17: this.catalogInfoCol.Add(new CatalogInfo() { CatalogName="Music & Video",CatalogComment="For Everyone Catalog" });
18: this.catalogInfoCol.Add(new CatalogInfo() { CatalogName = "Book References", CatalogComment = "just For Child" });
19: }
20:
21: public string catalogTitle;
22: public string CatalogTitle
23: {
24: get { return this.catalogTitle; }
25: set
26: {
27: this.catalogTitle = value;
28: base.NotifyPropertyChangedEventHandler("CatalogTitle");
29: }
30: }
34: }
35:
36: public class CatalogInfo
37: {
38: public string CatalogName{get;set;}
39: public string CatalogComment{get;set;}
40: }
這里定義一個ObserverCollection<T>來實現UI界面的綁定.數據源為了演示目的 采用靜態添加集合方式.添加數據.建立號ViewModel 添加UI綁定:
1: private MainPage_ViewModel mainPage_ViewModel = null;
2: void MainPage_Loaded(object sender, RoutedEventArgs e)
3: {
4: if (this.mainPage_ViewModel == null)
5: this.mainPage_ViewModel = new MainPage_ViewModel();
6: this.mainPage_ViewModel.LoadCatalogDefaultData();
7: this.DataContext = mainPage_ViewModel;
8: }
在UI中添加一個ListBox呈現數據直接運行效果如下:
至此一個簡單以MVVM形式構建分類列表顯示功能Windows phone 應用程序構建完成了.在構建單元測試之前.原結構化編程語言中,比如C,要進行測試的單元一般是函數或子過程.但在目前的OOP面向對象的概念中,單元測試對應基本單位就是類.但是實際操作發現.類作為測試單位,復雜度高,可操作性較差,因此仍然主張以函數作為單元測試的測試單位,但可以用一個測試類來組織某個類的所有測試函數. 相對於Windows phone 應用程序以MVVM模型以及UIBind引擎中.核心代碼更加傾向於集中ViewMolde和UI的Code-Behind中.因Silverlight Unit test FrameWork[SUTF]框架對單元測試具有可視化的輸出.所以必須基於Windows phone Application模板.要在單元測試的項目構建測試用例前.需要初始化SUTF測試結果用戶顯示界面.
在測試項目中UnitiyCommonEmptyDemo.Test的MainPage 的Loaded事件中需要做如下幾件事:
處理視圖:
[1]隱藏SystemTray系統托盤
[2]處理應用程序的BackPress事件在SUTF中建立單元輸出視圖切換
[3]把當SUTF的測試結果輸出當前UI中
實現如下:
1: void MainPageOutPut_Form_Loaded(object sender, RoutedEventArgs e)
2: {
3: //UnAvaliable SystemTray
4: SystemTray.IsVisible=false;
5: var currentMobileTestPage = UnitTestSystem.CreateTestPage() as IMobileTestPage;
6: if (currentMobileTestPage != null)
7: {
8: BackKeyPress += (x, se) => se.Cancel = currentMobileTestPage.NavigateBack();
9: (Application.Current.RootVisual as PhoneApplicationFrame).Content = currentMobileTestPage;
10: }
11: }
針對應用程序的功能.需要通過Unit TEst 驗證CatalogInfoCol是否觸發了PropertyChanged通知事件.綁定UI集合是否具有數據? 在修改CatalogTitle過程中是否正確傳遞屬性的值?.
有了如上兩個測試用例.針對對應MainpageUI建立MainPageTestHelper並表示類[TestClass]特性. 首先驗證CatalogInfoCol是否觸發通知事件.並在值發生變化集合中是否具有數據.建立第一個TEstCase:
1: [TestMethod]
2: public void DataColIsChanged_Test()
3: {
4: bool isPropertyChanged = false;
5: MainPage_ViewModel currentViewModel = new MainPage_ViewModel();
6: currentViewModel.PropertyChanged += (x, se) =>
7: {
8: if(currentViewModel.CatalogInfoCol.Count>0)
9: isPropertyChanged = true;
10: };
11: currentViewModel.CatalogInfoCol = new System.Collections.ObjectModel.ObservableCollection<CatalogInfo>()
12: {
13: new CatalogInfo(){CatalogName="ComplateTestChanged",CatalogComment="TestData"}
14: };
15: Assert.IsTrue(isPropertyChanged);
16: }
當對ViewModel屬性賦值觸發PropertyChanged事件.並判斷當前集合是否存在數據.同樣.修改CatalogTitle看額外的修改是否正確傳遞屬性對應的值,建立對應的Test Case 如下:
1: [TestMethod]
2: public void DataCatalogTitle_CatalogTitle_Test()
3: {
4: bool isEventChanged = false;
5: MainPage_ViewModel currentViewModel = new MainPage_ViewModel();
6: currentViewModel.PropertyChanged += (x, se) =>
7: {
8: if(currentViewModel.catalogTitle.Equals("newTitle"))
9: isEventChanged=true;
10: };
11: currentViewModel.CatalogTitle = "newTitle";
12: Assert.IsTrue(isEventChanged);
13: }
ok.編譯通過。運行結果:
在SUTF中對應類和函數 測試結果之間具有一定層級關系.點擊進入每個TestMethod具體的測試詳情:
well.也可以寫一個測試出錯的函數來看看在出錯是SUTF表現.添加TestCase 模擬出錯的情況 添加如下Code:
1: [TestMethod]
2: [Description("This test always fails intentionally")]
3: public void AllwaysWrong()
4: {
5: Assert.IsTrue(false,"Test Method For Wrong Case!");
6: }
編譯通過 運行:
帶有紅點是沒有通過測試的類.單擊類名可以找到類中帶有TestMethod特性的方法列表.能在測試結果詳情頁看到對應TestMethod對應Description描述,測試的結果 運行時間和對應的異常信息.而能在異常信息中也能看到我們Code預先設置出錯時會顯示ExceptionMessage字符串提示.
如上在Windows phone application 構建一個最簡單單元測試整個流程.
<2>異步操作
在Windows phone 應用開發中常常需要通過網絡協議獲取數據.或是通過異步操作實現常用UI更新等.這也是最為常見極為頻繁的異步操作.其實做過Silverlight Application集成測試的同學應該知道這往往大量異步操作照成測試過程很多難易規避的問題.
和大多數單元測試框架不同.Silverlight Unit Test FrameWork整個單元測試框架是運行相同的線程上的.如果應用程序引用任何外部服務類似一個WCF Service都需要一個返回的UI線程的異步調用. 導致在UT同一線程執行時無法阻止當前線程等待WCF調用返回結果.UT無法做.
針對Windows phone Application應用程序. 如果想做集成測試基本不太可能.Silverlight Unit Test Framework 常常因為進程之間互操作出現任何未處理的異常都會中斷整個集成測試的運行.而集成測試常常也需要長時間.跨越多線程操作的. 常常在運行時會出現異常后會自動跑到App.cs中Debugger.Break()方法中斷整個程序執行立即退出.沒有任何提示.而不是完全預期想UT測試返回Fail結果.
well.其實在Silverlight Unit Test Framework 框架對異步操作做UT完全可行的.只是存在一些測試用例中常常容易出錯問題.出錯頻率較高.如上應用擴展一下.把ViewModel中集合通過異步方式獲取數據源.
在獨立封裝UnitiyCommon 類庫中定義CommentAPI類用來獲取網絡上數據.定義Code如下:
1: public delegate void CommentData(List<CommentInfo> commentList, Exception se);
2: public static event CommentData LoadCommentDataComplated;
3:
4: /// <summary>
5: /// This Method Simulate asynchronous request
6: /// </summary>
7: /// <param name="uri">Request Download Image Uri</param>
8: public static void GetAllNewsCommentOperator(object uri)
9: {
10: if (!string.IsNullOrEmpty(uri.ToString()))
11: {
12: //Single Subscribe
13: LoadCommentDataComplated = null;
14: BasicAPI.TransportWebRequestOperator("POST", uri.ToString(), RequestComent_CallBack);
15: }
16: }
如上程序的目的通過一個指定的URI獲取網絡上圖片數據.這個過程是異步的.封裝類庫中.要UI進行交互則使用最原始簡單的委托+事件的組合方式.當圖片數據下載完成通過LoadCommentDataComplated事件通知執行操作. 下載圖片數據成功后.回調函數如下:
1: static void RequestComent_CallBack(IAsyncResult result)
2: {
3: try
4: {
5: HttpWebRequest currentRequest = result.AsyncState as HttpWebRequest;
6: WebResponse currentResponse = currentRequest.EndGetResponse(result);
7: if (currentResponse != null)
8: {
9: //Update State
10: IsComplated = true;
11: CommentInfo downloadComment = new CommentInfo()
12: {
13: CommentName = "Comment Image",
14: CommentImageUri=currentRequest.RequestUri.AbsoluteUri,
15: CommentImageData = currentResponse.GetResponseStream()
16: };
18: List<CommentInfo> commentList = new List<CommentInfo>(){downloadComment};
19: if (LoadCommentDataComplated != null)
20: LoadCommentDataComplated(commentList, null);
21: }
23: }
24: catch (Exception se)
25: {
26: if (LoadCommentDataComplated != null)
27: LoadCommentDataComplated(null, se);
28: }
29: }
回調函數手動處理數據.為了處理Unit Test單元測試.針對單元測試采用EnqueueCallback對象.需要額外添加如下Code:
1: public static bool IsComplated { get; private set; }
2: public void UpdateAsync()
3: {
4: System.Threading.ThreadPool.QueueUserWorkItem(GetAllNewsCommentOperator);
5: }
UpdateAsync方法的目的是通過Threadpool進程池的方式.在執行單元時調用.把所有的異步操作封裝隊列方式並稍后執行,.封裝號CommentAPI后.通過ViewModel與UI進行關聯.這里Code略去.詳見源碼.篇幅限制 不在贅述. 綁定UI后運行執行的結果如下:
如上其實我哪了一個最簡單而最常見WebRequest異步請求方式獲取網絡數據.如何在Silverlight Unit Test FrameWork中對這種異步操作做單元測試?
其實原來Silverlight Unit Test FrameWork在第一個版本時並不支持對異步操作.后來確實太多開發人員發現很多核心的業務在異步中無法實現UT.Jeff Wilcox在后續版本增加對異步操作支持 .關於實現的過程Jeff Wilcox在其博客中有一篇Blog說的非常清楚:
Asynchronous Support For SUTF:
Asynchronous test support – Silverlight unit test framework and the UI thread
在Silverlight Unit Test Framework執行過程隨着時間遷移執行如下:
從圖中輕易發現SUTF框架要面臨的問題,相對桌面Silverlight應用成不同的.SUTF要把可能在不同線程中異步調用操作.在時間軸能夠以類似同步方式按照隊列加以排序執行.關於這個執行規則組成.可以通過一系列UT中操作步驟完成. 那我們UT要完整測試一個異步調用 需要執行如下步驟:
異步測試需要執行的步驟:
[1]:首先通過線程池TheadPool把所有異步操作封裝.在隊列中隨着時間軸線稍后執行.在UT中通過調用該方法開發異步調用
[2]:EnqueueCallback()方法添加一個任務到執行隊列中.
[3]:EnqueueConditional()方法添加一個條件判斷隊列,如果為true才繼續執行
[4]: EnqueueDelay() –添加指定的隊列等待時間
[5]: EnqueueTestComplete() 添加一個TestComplete()到隊列中,這個方法告知framework測試結束了
具體的執行流程如下:
[如下章節.是在7份醉意下寫的. 有些細節可能寫的有些粗糙.]
梳理好了在測試框架中整個測試異步Begin-End模型流程.按照該流程執行.新建一個測試類MainPageAsyncTestHelper.首先針對異步測試需要引用常用的EnqueueCallback、EnqueueDelay等對象.該類需要繼承Microsoft.Silverlight.Testing;空間下SilverlightTes類.以便引用,實現核心Code:
1: [TestClass]
2: public class MainPageAsyncTestHelper:SilverlightTest
3: {
4: [TestMethod]
5: [Asynchronous]
6: [Description("Test Async Operator .")]
7: [Timeout(6000)]
8: public void AsyncOperator_ViewModel_Test()
9: {
10: CommentAPI currentCommentAPI = new CommentAPI();
11: bool isAsnycComplated = false;
12: CommentAPI.LoadCommentDataComplated += (x, se) =>
13: {
14: isAsnycComplated = true;
15: };
16:
17: //Test Async
18: EnqueueCallback(() => { currentCommentAPI.UpdateAsync(); });
19: EnqueueConditional(() => isAsnycComplated);
20: EnqueueCallback(() => Assert.IsFalse(CommentAPI.IsComplated));
21: EnqueueTestComplete();
22: }
在異步測試方法中.可選的特性項.針對異步操作測試方法必須添加[Asynchronous]標識.Description特性用來描述當前測試方法測試的功能簡介描述.
而對於Timeout用意.大家都應該知道Begin-End異步模型中.如果建立網絡請求可能導致請求超時情況發生.而且是服務器被動限制的.而在單元測試過程中.我們也不得不考慮當前單元測試可能會失敗.可能在執行異步過程中會卡在一個無線循環或是類似請求的狀態中.此類狀態會使測試的執行耗費太長的時間.特別在執行集成測試中這種現象最為明顯和常見.當然作為單元測試.盡量保證功能完整正確.特別在使用ASynchronous特性標識.如果在執行EnqueueConditional時從未使其條件語句為真.導致測試用例可能會被無限期鎖住.當然為了是測試流程中避免出現中斷測試或測試用例無法全部執行下去情況發生.Timeout特性為執行測試方法的時間提供一個上限值. 如果測試方法超過該時間則認定為失敗.
Well.通過測試用例中.首先建立一個標識屬性isAsnycComplated 用來標識當前異步操作是否完成.這是作為EnqueueCallBAck對象執行隊列中必備的執行條件.首先通過UpdateAsync()方法啟動異步方法. 再通過IsAsnyncComplated指定執行條件. Assert對齊進行排列.最后通過EnqueueComplete()方法來指示當前測試方法結束.
編譯通過.測試結果:
異步測試通過.
<4>小結
本篇其實原不想寫這么多篇幅.在Windows phone 中開始做Unit Test和集成測試也因傳統的異步Begin-End模型會在實際操作出現很多異常.本篇目的是演示Windows phone 中做UT主要方式.以及處理這個過程自己碰到一些具體問題尋求的實際解決方案.拋磚引玉.但目前集成測試中一個解決方案是始終通過EnqueueCallback確保異常恰當地報告給單元測試框架。只要一個錯誤就能中斷接下來的所有測試.而引起這個問題根源主要源於Windows phone很多操作異步模型導致.當然關於集成測試出錯比較頻繁的情況.國外一個作者Richard Szalay在其通過RX[Reactive Extensions]結合單元測試 給出一個處理解決方案. 這篇文章鏈接如下:
Richard Szalay 集成解決方案:
Writing asynchronous unit tests with Rx and the Silverlight Unit Testing Framework
在實際開發中其實我們項目中采用三種測試框架.Silverlight Unit Test Framework采用的最為廣泛. 但SUTf依然存在很多限制和需要改善的地方。下篇將介紹通過其他第三方框架更簡潔實現UT.並總結相對SUTF具有優勢和特點.
關於本盤如果任何問題 請在評論中指出.
本篇所有演示的源碼下載地址:/Files/chenkai/UnitiyCommonDirDemo.rar
參考資料: