Windows phone 應用開發[9]-單元測試

l_serein發表於2012-10-30

本篇來談談Windows phone Unit Test.

原來在9月份一次線下技術沙龍現場交流.我在現場提到關於Windows phone Unit Test在實際程式設計所體現一些問題.可惜當時在現場回應人的太少.通過本篇將詳細梳理關於在Windows phone 開發流程做UT可能遇到的問題,以及一些具體解決方案.

關於UT.不會在這裡拿太多篇幅解釋它基本的用法.當然也更不會拿時間去強調UT它在實際程式設計中保證軟體質量重要性.從自身角度來說.一個程式設計師良好的職業素養往往源自於對自身高要求,並能持之以恆的保持下去.在實際開發流程照成很多”不愉快“的體驗,其實很多從自身角度來說完全可以避免的.

software_developer

其實很多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 AndUnit Testing Silverlight & Windows Phone Applications

[2]通過第三方測試框架Windows phone Test FrameWorkNUnit 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引用關係.這時會提示:

2012-01-04_180856

提示引用Silverlight類庫.可以不理會提示直接點選是確定引用.

方式二: 開啟Visual Studio 2011 找到Tool->Extension Manager .在Online Gallery選項中搜尋:Windows phone Test Project 可以看到:

2012-01-05_192531

可以直接通過點選Download下載安裝該專案模板.安裝完成後新建Project就能看到 Test選項頁下多了一個Windows phone Test Project模板:

2012-01-05_193307

新建一個測試專案命名Test project1[測試用].在執行編譯前需要安裝Nuget然後通過Tool->Library Package Manager->Package Manager Console視窗輸入如下命令新增引用:

2012-01-05_194039

這時會在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呈現資料直接執行效果如下:

2012-01-06_110013

至此一個簡單以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.編譯通過。執行結果:

2012-01-06_1202092012-01-06_1202242012-01-06_120233

在SUTF中對應類和函式 測試結果之間具有一定層級關係.點選進入每個TestMethod具體的測試詳情:

2012-01-06_135056

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:  }

編譯通過 執行:

2012-01-06_1355172012-01-06_1355252012-01-06_140737

帶有紅點是沒有通過測試的類.單擊類名可以找到類中帶有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後執行執行的結果如下:

2012-01-06_215219

如上其實我哪了一個最簡單而最常見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執行過程隨著時間遷移執行如下:

ComparingExecution

從圖中輕易發現SUTF框架要面臨的問題,相對桌面Silverlight應用成不同的.SUTF要把可能在不同執行緒中非同步呼叫操作.在時間軸能夠以類似同步方式按照佇列加以排序執行.關於這個執行規則組成.可以通過一系列UT中操作步驟完成. 那我們UT要完整測試一個非同步呼叫 需要執行如下步驟:

非同步測試需要執行的步驟:

[1]:首先通過執行緒池TheadPool把所有非同步操作封裝.在佇列中隨著時間軸線稍後執行.在UT中通過呼叫該方法開發非同步呼叫

[2]:EnqueueCallback()方法新增一個任務到執行佇列中.

[3]:EnqueueConditional()方法新增一個條件判斷佇列,如果為true才繼續執行

[4]: EnqueueDelay() –新增指定的佇列等待時間

[5]: EnqueueTestComplete() 新增一個TestComplete()到佇列中,這個方法告知framework測試結束了

具體的執行流程如下:

WorkItemExample2

[如下章節.是在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()方法來指示當前測試方法結束.

編譯通過.測試結果:

2012-01-06_2150202012-01-06_2150322012-01-06_215037

非同步測試通過.

<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

參考資料:

Writing asynchronous unit tests with Rx and the Silverlight Unit Testing Framework

A Cheat Sheet for Unit Testing Silverlight Apps on Windows Phone 7

Asynchronous test support – Silverlight unit test framework and the UI thread
Running Windows Phone Unit Tests via MSBuild

相關文章