本篇来谈谈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越来越瘦的趋势.但必要测试依然是构造可靠应用程序必经之路.

针对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 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引用关系.这时会提示:

提示引用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. public void LoadCatalogDefaultData()   
  14. {   
  15. this.catalogInfoCol.Clear();   
  16. this.catalogInfoCol.Add(new CatalogInfo() { CatalogName="Music & Video",CatalogComment="For Everyone Catalog" });   
  17. this.catalogInfoCol.Add(new CatalogInfo() { CatalogName = "Book References", CatalogComment = "just For Child" });   
  18. }   
  19. public string catalogTitle;   
  20. public string CatalogTitle   
  21. {   
  22. get { return this.catalogTitle; }   
  23. set   
  24. {   
  25. this.catalogTitle = value;   
  26. base.NotifyPropertyChangedEventHandler("CatalogTitle");   
  27. }   
  28. }   
  29. }   
  30. public class CatalogInfo   
  31. {   
  32. public string CatalogName{get;set;}   
  33. public string CatalogComment{get;set;}   

这里定义一个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;   

在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. }   

针对应用程序的功能.需要通过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);   
当对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);   

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!");   

编译通过 运行:

带有红点是没有通过测试的类.单击类名可以找到类中带有TestMethod特性的方法列表.能在测试结果详情页看到对应TestMethod对应Description描述,测试的结果 运行时间和对应的异常信息.而能在异常信息中也能看到我们Code预先设置出错时会显示ExceptionMessage字符串提示.

如上在Windows phone application 构建一个最简单单元测试整个流程.

在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. }   

如上程序的目的通过一个指定的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. };   
  17. List<CommentInfo> commentList = new List<CommentInfo>(){downloadComment};   
  18. if (LoadCommentDataComplated != null)   
  19. LoadCommentDataComplated(commentList, null);   
  20. }    
  21. }   
  22. catch (Exception se)   
  23. {   
  24. if (LoadCommentDataComplated != null)   
  25. LoadCommentDataComplated(null, se);   
  26. }   

回调函数手动处理数据.为了处理Unit Test单元测试.针对单元测试采用EnqueueCallback对象.需要额外添加如下Code:


  1. public static bool IsComplated { get; private set; }   
  2. public void UpdateAsync()   
  3. {   
  4. System.Threading.ThreadPool.QueueUserWorkItem(GetAllNewsCommentOperator);   

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();    

在异步测试方法中.可选的特性项.针对异步操作测试方法必须添加[Asynchronous]标识.Description特性用来描述当前测试方法测试的功能简介描述.

而对于Timeout用意.大家都应该知道Begin-End异步模型中.如果建立网络请求可能导致请求超时情况发生.而且是服务器被动限制的.而在单元测试过程中.我们也不得不考虑当前单元测试可能会失败.可能在执行异步过程中会卡在一个无线循环或是类似请求的状态中.此类状态会使测试的执行耗费太长的时间.特别在执行集成测试中这种现象最为明显和常见.当然作为单元测试.尽量保证功能完整正确.特别在使用ASynchronous特性标识.如果在执行EnqueueConditional时从未使其条件语句为真.导致测试用例可能会被无限期锁住.当然为了是测试流程中避免出现中断测试或测试用例无法全部执行下去情况发生.Timeout特性为执行测试方法的时间提供一个上限值. 如果测试方法超过该时间则认定为失败.

Well.通过测试用例中.首先建立一个标识属性isAsnycComplated 用来标识当前异步操作是否完成.这是作为EnqueueCallBAck对象执行队列中必备的执行条件.首先通过UpdateAsync()方法启动异步方法. 再通过IsAsnyncComplated指定执行条件. Assert对齐进行排列.最后通过EnqueueComplete()方法来指示当前测试方法结束.

编译通过.测试结果:

异步测试通过.

本篇其实原不想写这么多篇幅.在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具有优势和特点.

关于本盘如果任何问题 请在评论中指出.

本篇所有演示的源码见附件。

参考资料:

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