咱们接上回书说,上一回我们从概念上详细介绍了ToolkitTests自动测试框架的架构设计,可能是太理论了,感觉似乎那篇文章没几个人看明白了,起码针对那篇文章本身一点儿让人热心沸腾的反馈都没有,整篇文章对读者的影响还不如最后的"猛击这一行",哎!这篇文章开始介绍整个ToolkitTests的实现细节,吸取上一回的教训,这次的代码实例讲解会比较多(整天写代码的人,还是跟代码亲啊),不过需要强调的是,要深刻理解ToolkitTests,上一回的理论还是很重要的,如果你还没有看的话,建议你先去看看再回来,带着你的问题来这篇文章找答案会更好些!
先说说准备工作吧
我推荐下载官方的AjaxControlToolkit项目,下载地址是:VS2008版本、VS2005版本,VS2008版本的不需要什么特殊的配置,直接打开解压后的项目文件就行了,VS2005 版本的可能要麻烦一点,具体可以参考这篇文章;不管你看的是那个版本的源代码,对于ToolkitTests项目来说没有什么区别,用VS打开你下载到的源代码,里面有一个名为ToolkitTests的Web项目,那就是我们一直在讲解的ToolkitTests自动测试框架,并且里面已经预置好的所有AjaxControlToolkit扩展控件的单元测试案例了!如果你嫌下载官方的版本比较费劲(你真够懒的),可以下载我精简过后的VS2008版本的ToolkitTests项目,运行起来效果也是一样的,最主要的是,精简版本里的TestHarness.js文件的注释都已经提供对应的中文翻译,可以帮助你理解整个代码的逻辑。ToolkitTests项目运行起来的效果如下图:
运行一个测试案例
按上面的步骤,做好准备工作以后,我们先实际运行一个测试案例,这里我选择了一个比较简单也是比较常用的Calendar控件的单元测试,运行过程很简单,在上图用绿框表示的待测试案例(TestItem)列表中选择Calendar案例,然后选择执行“Run Tests”按钮,自动测试框架就会执行Calendar测试套件定义的测试方案的所有步骤,测试执行过程中,你会看到在自动测试框架的显示结果区域(TestResults),框架在自动模拟操作Calendar控件的效果,最后给出测试结果,这个过程完全是不需要人为干预的,从前到后如下图五步动画所示:
逐层分解各个环节
看了上面Calendar单元测试案例执行的动画效果,是不是有些热血澎湃了呢,如果你看了所有测试案例一起全部自动执行的效果,就更按捺不住啦!是不是很想知道这个过程是怎么实现的呢?别急,且听我从头开始为你解析每一个环节!我们再看看上回书说到的整个架构的关键抽象图,我会按图中元素逐个分解实现细节。
AutoTestMainFrom
AutoTestMainFrom 只是一个抽象的命名,在ToolkitTests自动测试框架中,它就是整个自动测试框架的主界面,它对应的物理文件其实就是 Default.aspx(cs),咱们看代码来说吧,下面是Default.aspx的源代码:


2 Language = " C# "
3 AutoEventWireup = " true "
4 CodeFile = " Default.aspx.cs "
5 Inherits = " Automated_TestHarness " %>
6 <! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
7 < html xmlns ="http://www.w3.org/1999/xhtml" >
8 < head runat ="server" >
9 < title > AJAX Control Toolkit Test Harness </ title >
10 < link href ="Default.css" rel ="stylesheet" type ="text/css" />
11 < script language ="Javascript" type ="text/javascript" src ="TestHarness.js" ></ script >
12 </ head >
13 < body onload ="testHarness.initialize();" >< form runat ="server" >< div >
14 < script language ="javascript" type ="text/javascript" >
15 // (c) Copyright Microsoft Corporation.
16 // This source is subject to the Microsoft Public License.
17 // See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL.
18 // All other rights reserved.
19
20 // List of test suite URLs populated by enumerating the directory server-side
21 var testSuiteUrls = [ < asp:Literal ID = " litTestSuiteUrls " runat = " server " / >];
22
23 // Command line flags set by the server
24 var runAll = < asp:Literal ID = " litRunAllFlag " runat = " server " / >;
25 var debug = < asp:Literal ID = " litDebugFlag " runat = " server " / >;
26 </ script >
27 < table width ="100%" height ="100%" style ="text-align: center;" >
28 < tr >< td colspan ="2" >
29 < h1 > AJAX Control Toolkit Automated Test Harness </ h1 >
30 < div id ="status" class ="status" >
31 Passed: < span id ="statusPassed" > 0 </ span > , Failed: < span id ="statusFailed" > 0 </ span > , Not Run: < span id ="statusUnknown" > 0 </ span >
32 </ div >
33 </ td ></ tr >
34 < tr >
35 < td valign ="top" >
36 < div class ="testSuiteList" >
37 < div id ="availableTests" style ="width: 100%; text-align: left; overflow: auto;" runat ="server" />< br />
38 < div >
39 < a href ="#" onclick ="testHarness.selectAllTests(true);" > Select All </ a > |
40 < a href ="#" onclick ="testHarness.selectAllTests(false);" > Clear All </ a >
41 </ div >< br />
42 < input type ="button" id ="btnRun" value ="Run Tests" onclick ="testHarness.runTests();" />< br />< br />
43 < div >< input type ="checkbox" id ="chkDebug" />< label for ="chkDebug" > Debug </ label ></ div >
44 </ div >
45 </ td >
46 < td valign ="top" width ="100%" >
47 < iframe id ="wndTest" src ="about:blank" width ="100%" height ="400" style ="margin-bottom: 15px;"
48 onload ="window.setTimeout(testHarness.initializeTestSuite, testHarness.Constants.InitialDelay);" ></ iframe >
49 < div id ="results" style ="width: 100%; text-align: left;" runat ="server" />
50 </ td >
51 </ tr >
52 </ table >
53 </ div ></ form ></ body >
54 </ html >
简单分析Default.aspx的源代码就会知道,这个页面就是一个“空壳”,自动测试相关的核心逻辑全部封装在客户端对象testHarness中了,这部分我们后面会说到。代码很简单,这里只强调一下代码里的核心部分:(1)页面从服务器端接收了三个字符串变量testSuiteUrls、runAll、debug,它们都被从服务器端直接转换成了客户端的全局变量,供testHarness对象调用。(2)<body onload="testHarness.initialize();">说明窗体每次加载的时候,都会初始化 testHarness。(3)<iframe ... onload="window.setTimeout(testHarness.initializeTestSuite, testHarness.Constants.InitialDelay);">这段代码可以说是最关键的部分,它的作用就是加载具体的测试案例,整个自动测试框架要是没有它可就玩不转了。下面看看Default.aspx.cs的源代码:


2 // This source is subject to the Microsoft Public License.
3 // See http://www.microsoft.com/opensource/licenses.mspx #Ms-PL.
4 // All other rights reserved.
5
6 using System;
7 using System.Collections.Generic;
8 using System.IO;
9 using System.Web;
10 using System.Web.UI;
11 using System.Text;
12
13 public partial class Automated_TestHarness : Page
14 {
15 /// <summary>
16 /// Populate the array of Test Suite Urls
17 /// </summary>
18 protected void Page_Load( object sender, EventArgs e)
19 {
20 // int size = System.Runtime.InteropServices.Marshal.SizeOf(typeof(Guid));
21
22 // Create the list of test suites and add the basics first
23 // 加载测试案例列表,将默认的测试案例排在最顶端
24 List < string > testSuites = new List < string > ();
25 testSuites.Add( " 'TestHarnessTests.aspx' " );
26 testSuites.Add( " 'ExtenderBase.aspx' " );
27
28 // Dynamically add the rest of the *.aspx files in this directory as tests
29 foreach ( string path in Directory.GetFiles(Server.MapPath( " ~ " ), " *.aspx " ))
30 {
31 string file = Path.GetFileName(path);
32 if ( string .Compare(file, " Default.aspx " , StringComparison.OrdinalIgnoreCase) != 0 &&
33 string .Compare(file, " TestHarnessTests.aspx " , StringComparison.OrdinalIgnoreCase) != 0 &&
34 string .Compare(file, " ExtenderBase.aspx " , StringComparison.OrdinalIgnoreCase) != 0 )
35 {
36 testSuites.Add( string .Format( " '{0}' " , file));
37 }
38 }
39
40 // Pass the test suite URLs back to the client
41 litTestSuiteUrls.Text = string .Join( " , " , testSuites.ToArray());
42
43 // Look for a querystring flag to automatically run all the tests at once
44 bool runAll = false ;
45 bool .TryParse(Request.QueryString[ " RunAll " ], out runAll);
46 litRunAllFlag.Text = runAll.ToString().ToLower();
47
48 // Look for a querystring flag to run the tests in "debug mode"
49 bool debug = false ;
50 bool .TryParse(Request.QueryString[ " Debug " ], out debug);
51 litDebugFlag.Text = debug.ToString().ToLower();
52 }
53 }
服务端的代码就做了一件事,给客户端传递那三个字符串变量testSuiteUrls、runAll、debug,其中runAll和debug是在 Request.QueryString里传递过了的,也就是在调用框架的时候,在地址栏里如果输入了?RunAll=true& Debug=true,服务端就会将这两个值传递给客户端的testHarness对象,其中runAll的作用是,如果这个值为true,那么测试框架默认会一启动就执行所有测试案例的自动化测试,而debug的作用是设置执行测试的时候的操作模式,如果设置为true,则在testHarness内部不会对测试方案(TestCase)内部产生的异常进行try...catch处理,那么如果是通过runAll参数,在无人监控的情况下,通过命令行指定的批量自动执行所有测试案例的时候,就会造成因异常而中断所有测试,所以这种情况下应该设置debug为false(默认)。这里设置 testSuiteUrls变量的方式有点意思,testSuiteUrls的内容就是通过逗号分隔的所有单元测试案例(TestItem)的文件名,这里单元测试案例的物理表现形式就是具体的*.aspx文件,toolkitTests框架初始化的时候,在服务器端通过遍历相同目录下的所有*.aspx 文件来加载单元测试案例,从这个角度说,toolkitTest框架的所有自定义的单元测试案例都是即插即用的(需要F5一次),只要将写好的*.aspx文件拷贝到对应目录里就好了,不需要再修改任何配置文件或其他什么特殊的设置,这种非常低的耦合关系真的是帅呆了!
AutoTestMainFrom介绍这么多应该差不多了,这部分的核心内容也就这些,它们主要是在配合使用TestHarness罢了,所以没什么难理解的。
TestItem & RegisterTests
在正式开始介绍TestHarness的实现之前,我们先来介绍一下TestItem,这个名字是我在上一篇文章里起的,意思就是一个测试单元条目,你可以把它理解为一个单元测试包或者一个可插拔的测试组件。在toolkitTests框架中,TestItem角色对应的就是需要编写单元测试的开发人员来实现的*.aspx文件,因为我们的toolkitTests框架针对的测试对象是在浏览器端运行的ASP.NET控件,而这些控件都是在*.aspx上使用的,所以设计师将TestItem的表现形式定性成了*.aspx形式。*.aspx文件里的registerTests方法是TestItem于 toolkitTests框架之间的君子协定,是它们之间通信的唯一接口,如果在TestItem里不存在registerTests方法,那么 toolkitTests框架不会执行任何实际的单元测试。打个比方来说,toolkitTests框架就像是一台DVD播放器,它的能力只可以播放 DVD或CD光盘上的影音内容,所以如果没有光盘或者放进去的不是DVD或CD光盘,那就什么东西也不可能播放出来,这里含有registerTests 方法的*.aspx文件就是DVD或CD光盘,把它放到toolkitTests播放器里,就可以播放出自动化测试案例的结果。
我们以Calendar测试案例为例,来看看其内部都做了什么。概括地说,Calendar测试案例就是创建了一个使用的Calendar控件的aspx 页面,然后再用JavaScript写了一些在客户端使用Calendar控件的代码,这些代码的作用就是测试Calendar控件的行为是否跟单元测试案例预期的一样,这里最有意思的地方就是,使用JavaScript调用Calendar控件的客户端对象,可以写出模拟人为操作Calendar控件的效果,当然这也是整个toolkitTests自动测试框架可以像现在这样来设计和运行的重要基础之一。下面看看Calendar单元测试案例的源代码:


2 Language = " C# "
3 MasterPageFile = " ~/Default.master "
4 Title = " Automated Calendar "
5 CodeFile = " Calendar.aspx.cs "
6 Inherits = " Automated_Calendar "
7 Culture = " hi-in " %>
8 < asp:Content ID = " Content1 " ContentPlaceHolderID = " ContentPlaceHolder1 " Runat = " Server " >
9
10 <!-- harness -->
11 < asp:TextBox runat = " server " ID = " Text " />
12 < ajaxToolkit:CalendarExtender runat = " Server " BehaviorID = " Calendar " TargetControlID = " Text " />
13 < asp:Button runat = " server " ID = " Button " OnClientClick = " return false; " />
14 < script type = " text/javascript " >
15
16 // (c) Copyright Microsoft Corporation.
17 // This source is subject to the Microsoft Public License.
18 // See http://www.microsoft.com/opensource/licenses.mspx #Ms-PL.
19 // All other rights reserved.
20
21 // Script objects that should be loaded before we run
22 var typeDependencies = [ ' AjaxControlToolkit.CalendarBehavior ' ];
23
24 function registerTests(harness) {
25 var text = harness.getElement( ' ctl00_ContentPlaceHolder1_Text ' );
26 var calendar = harness.getObject( ' Calendar ' );
27 var button = harness.getElement( ' ctl00_ContentPlaceHolder1_Button ' );
28 var test = null ;
29
30 // 第一个测试方案
31 test = harness.addTest( ' Show on focus ' );
32 test.addStep(function() {
33 harness.fireEvent(text, " onfocus " );
34 harness.assertTrue(calendar._isOpen);
35 });
36
37 // 第二个测试方案
38 test = harness.addTest( ' Hide on blur ' );
39 test.addStep(function() {
40 harness.fireEvent(text, " onfocus " );
41 harness.fireEvent(text, " onblur " );
42 }, 100 , function() { return ! calendar._isOpen; });
43
44 // 第三个测试方案
45 test = harness.addTest( ' Parse date ' );
46 test.addStep(function() {
47 text.value = ' 15-1-2000 ' ; // 设置文本框的值
48 harness.fireEvent(text, " onfocus " ); // 让text获得焦点
49 harness.fireEvent(text, ' onchange ' ); // 触发text的改变事件
50 harness.assertEqual( ' 15-1-2000 ' , calendar.get_selectedDate().format( " d-M-yyyy " )); // 校验calender里的值是否等于文本框里的值
51 });
52
53 // 第四个测试方案
54 test = harness.addTest( ' set_firstDayOfWeek typo ' );
55 test.addStep(function() {
56 // 设置每周的第一天是星期日还是星期一
57 calendar.set_firstDayOfWeek(AjaxControlToolkit.FirstDayOfWeek.Default);
58 });
59 }
60
61 </ script >
62
63 </ asp:Content >
代码里我已经加了注释,简单的说,整个Calendar.aspx就是一个单元测试案例(TestItem),它的作用就是封装了多个针对 Calendar扩展控件的测试方案(TestCase),从代码里我们可以看到,通过registerTests方法,Calendar.aspx一共注册了四个测试方案,第一个方案是测试当文本框获得焦点的时候,是否会弹出选择日期框,对应[动画1];第二个方案是测试当焦点离开文本框的时候,弹出的日期框会不会消失,对应[动画2];第三个方案是测试给文本框设置一个日期,对应的选择日期框里的值是否会改变,对应[动画3];第四个方案是测试调用设置每周的第一天是星期日还是星期一的函数,对应[动画4],这个看不出什么效果;所有测试方案都执行完后,框架会将结果统一显示在结果区域,对应[动画 5],这个例子只预置了四个测试方案,如果有需要还可以预置更多的测试方案,可以把Calendar控件的所有属性和方法测试个遍。当然,这个单元测试的编写工作必须有对Calendar控件的使用接口非常熟悉的人来完成了,或者说就应该是控件的设计者和实现者自己最好。所以,如果以后我们封装了一套自己的AjaxControlToolkit控件,也同样可以自己写一套单元测试放到toolkitTests框架里来自动运行了。这里设置了一个特殊的变量 typeDependencies,它的作用就是告诉框架,运行这个测试案例之前,还有那些依赖的客户端对象必须先已经存在,如果依赖性还不存在,框架会延时执行测试案例,直到依赖性都加载完为止。关于TestItem就说这么多,TestItem是根据测试目标的不同而特别订制的配件,其实现细节完全看定制者的功力和目标了,toolkitTests中预置了所有AjaxControlToolkit项目里控件的单元测试案例,这部分暂时留给大家自己去挖掘,以后随着我个人对每个控件学习的深入,会再展开介绍。
本来想一篇文章把所有关键元素的实现细节都说一遍的,但是写到这里,时间已经过去3个小时了,现在只完成了原来预想的1/2不到,后面的内容更为核心了,更应该展开来细说,那么再有这些内容都挡不住,我看还是把剩下的内容放在下一篇文章里吧。通过这篇文章,结合上一篇,我相信你应该对ToolkitTests自动测试框架和我到底在说什么有一个清楚的认识了吧,我想看过这篇文章后,你最应该做的就是下载我精简过后的VS2008版本的ToolkitTests项目,先通读一遍 TestHarness.js文件,里面的注释,除了一些非常简单且不重要的以外,我已经全部翻译成中文了(我翻译了3、4小时,不一定完全准确,欢迎挑错。),你如果对ToolkitTests的实现细节感兴趣,那就好好读读源代码和注释吧,下一篇文章我们要说的内容都在里面了,它们是 TestHarness、TestSuit & TestCase & TestStep、TestInterface & TestResults。